We have a view with an IndexTable with an IndexFilter on our application future landing page and we face this bug:
On mobile screen when the user scroll the IndexFilter is badly positionned and it makes the IndexTable unusable. The bug is easily reproduceable in the Polaris Storybook.
Here is a screenshot of the behavior:
To make the fix easier we have created this simple component below that is reproducing it as well.
import {useCallback, useEffect, useState} from "react";
import {
Card,
ChoiceList,
IndexFilters,
IndexFiltersMode, IndexTable,
RangeSlider,
Text,
TextField, useIndexResourceState,
useSetIndexFiltersMode
} from "@shopify/polaris";
export default function Testindextablewithindexfilter() {
function Table() {
const customers = [
{
id: '3411',
url: '#',
name: 'Mae Jemison',
location: 'Decatur, USA',
orders: 20,
amountSpent: '$2,400',
},
{
id: '2561',
url: '#',
name: 'Ellen Ochoa',
location: 'Los Angeles, USA',
orders: 30,
amountSpent: '$140',
},
{
id: '1412',
url: '#',
name: 'Sarah Connor',
location: 'Dublin, Ireland',
orders: 27,
amountSpent: '$1,890',
},
{
id: '1523',
url: '#',
name: 'Lars Nielsen',
location: 'Copenhagen, Denmark',
orders: 31,
amountSpent: '$2,340',
},
{
id: '1634',
url: '#',
name: 'Maria Garcia',
location: 'Barcelona, Spain',
orders: 44,
amountSpent: '$3,450',
},
{
id: '1745',
url: '#',
name: 'Alexander Petrov',
location: 'Moscow, Russia',
orders: 29,
amountSpent: '$2,100',
},
{
id: '1856',
url: '#',
name: 'Isabella Santos',
location: 'Lisbon, Portugal',
orders: 36,
amountSpent: '$2,780',
},
{
id: '1967',
url: '#',
name: 'Hans Weber',
location: 'Berlin, Germany',
orders: 41,
amountSpent: '$3,120',
},
{
id: '2078',
url: '#',
name: 'Sophia Chen',
location: 'Singapore',
orders: 48,
amountSpent: '$3,890',
},
{
id: '2189',
url: '#',
name: 'Oliver Brown',
location: 'Sydney, Australia',
orders: 34,
amountSpent: '$2,560',
},
{
id: '2290',
url: '#',
name: 'Aisha Patel',
location: 'Mumbai, India',
orders: 25,
amountSpent: '$1,670',
},
{
id: '2301',
url: '#',
name: 'Lucas Martinez',
location: 'Mexico City, Mexico',
orders: 39,
amountSpent: '$2,890',
},
{
id: '2412',
url: '#',
name: 'Eva Andersson',
location: 'Stockholm, Sweden',
orders: 45,
amountSpent: '$3,450',
},
{
id: '2523',
url: '#',
name: 'Thomas Wilson',
location: 'Toronto, Canada',
orders: 32,
amountSpent: '$2,340',
},
{
id: '2634',
url: '#',
name: 'Nina KovaÄŤ',
location: 'Zagreb, Croatia',
orders: 21,
amountSpent: '$1,230',
},
{
id: '2745',
url: '#',
name: 'David Cohen',
location: 'Tel Aviv, Israel',
orders: 37,
amountSpent: '$2,780',
},
{
id: '2856',
url: '#',
name: 'Fatima Al-Sayed',
location: 'Cairo, Egypt',
orders: 28,
amountSpent: '$1,890',
},
{
id: '2967',
url: '#',
name: 'Jack Williams',
location: 'Auckland, New Zealand',
orders: 43,
amountSpent: '$3,230',
},
{
id: '3078',
url: '#',
name: 'Elena Popov',
location: 'Bucharest, Romania',
orders: 26,
amountSpent: '$1,780',
},
{
id: '3189',
url: '#',
name: 'Mohammed Al-Hassan',
location: 'Riyadh, Saudi Arabia',
orders: 35,
amountSpent: '$2,670',
},
{
id: '3290',
url: '#',
name: 'Julia Berg',
location: 'Vienna, Austria',
orders: 49,
amountSpent: '$3,890',
},
{
id: '3301',
url: '#',
name: 'Daniel Kim',
location: 'Seoul, South Korea',
orders: 52,
amountSpent: '$4,120',
},
];
const resourceName = {
singular: 'customer',
plural: 'customers',
};
const {selectedResources, allResourcesSelected, handleSelectionChange} =
useIndexResourceState(customers);
const rowMarkup = customers.map(
({id, name, location, orders, amountSpent}, index) => (
<IndexTable.Row
id={id}
key={id}
selected={selectedResources.includes(id)}
position={index}
>
<IndexTable.Cell>
<Text variant="bodyMd" fontWeight="bold" as="span">
{name}
</Text>
</IndexTable.Cell>
<IndexTable.Cell>
<Text as="span" variant="bodyMd">
{location}
</Text>
</IndexTable.Cell>
<IndexTable.Cell>
<Text as="span" variant="bodyMd">
{orders}
</Text>
</IndexTable.Cell>
<IndexTable.Cell>
<Text as="span" variant="bodyMd">
{amountSpent}
</Text>
</IndexTable.Cell>
</IndexTable.Row>
),
);
return (
<IndexTable
resourceName={resourceName}
itemCount={customers.length}
selectedItemsCount={
allResourcesSelected ? 'All' : selectedResources.length
}
onSelectionChange={handleSelectionChange}
headings={[
{title: 'Name'},
{title: 'Location'},
{title: 'Order count'},
{title: 'Amount spent'},
]}
>
{rowMarkup}
</IndexTable>
);
}
const sleep = (ms) =>
new Promise((resolve) => setTimeout(resolve, ms));
const [itemStrings, setItemStrings] = useState([
'All',
'Unpaid',
'Open',
'Closed',
'Local delivery',
'Local pickup',
]);
const deleteView = (index) => {
const newItemStrings = [...itemStrings];
newItemStrings.splice(index, 1);
setItemStrings(newItemStrings);
setSelected(0);
};
const duplicateView = async (name) => {
setItemStrings([...itemStrings, name]);
setSelected(itemStrings.length);
await sleep(1);
return true;
};
const tabs = itemStrings.map((item, index) => ({
content: item,
index,
onAction: () => {},
id: `${item}-${index}`,
isLocked: index === 0,
actions:
index === 0
? []
: [
{
type: 'rename',
onAction: () => {},
onPrimaryAction: async (value) => {
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) => {
await sleep(1);
deleteView(index);
return true;
},
},
],
}));
const [selected, setSelected] = useState(0);
const onCreateNewView = async (value) => {
await sleep(500);
setItemStrings([...itemStrings, value]);
setSelected(itemStrings.length);
return true;
};
const 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 =
selected === 0
? {
type: 'save-as',
onAction: onCreateNewView,
disabled: false,
loading: false,
}
: {
type: 'save',
onAction: onHandleSave,
disabled: false,
loading: false,
};
const [accountStatus, setAccountStatus] = useState(null);
const [moneySpent, setMoneySpent] = useState(null);
const [taggedWith, setTaggedWith] = useState('');
const [queryValue, setQueryValue] = useState('');
const [uncontrolledLoading, setLoading] = useState(false);
const loading = uncontrolledLoading;
// Psuedo-loading state transitions
useEffect(() => {
if (queryValue !== '') {
setLoading(true);
}
const timeoutId = setTimeout(() => {
setLoading(false);
}, 1500);
return () => clearTimeout(timeoutId);
}, [queryValue]);
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: (
<ChoiceList
title="Account status"
titleHidden
choices={[
{label: 'Enabled', value: 'enabled'},
{label: 'Not invited', value: 'not invited'},
{label: 'Invited', value: 'invited'},
{label: 'Declined', value: 'declined'},
]}
selected={accountStatus || []}
onChange={handleAccountStatusChange}
allowMultiple
/>
),
shortcut: true,
},
{
key: 'taggedWith',
label: 'Tagged with',
filter: (
<TextField
label="Tagged with"
value={taggedWith}
onChange={handleTaggedWithChange}
autoComplete="off"
labelHidden
/>
),
shortcut: true,
},
{
key: 'moneySpent',
label: 'Money spent',
filter: (
<RangeSlider
label="Money spent is between"
labelHidden
value={moneySpent || [0, 500]}
prefix="$"
output
min={0}
max={2000}
step={1}
onChange={handleMoneySpentChange}
/>
),
},
];
const 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 (
<Card padding="0">
<IndexFilters
loading={loading}
sortOptions={sortOptions}
sortSelected={sortSelected}
queryValue={queryValue}
queryPlaceholder="Searching in all"
onQueryChange={handleFiltersQueryChange}
onQueryClear={() => setQueryValue('')}
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}
/>
<Table />
</Card>
);
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;
}
}
}

