Skip to content

Commit

Permalink
Add hidden property FilterInterface (#11269)
Browse files Browse the repository at this point in the history
<!--
  ☝️How to write a good PR title:
- Prefix it with [ComponentName] (if applicable), for example: [Button]
  - Start with a verb, for example: Add, Delete, Improve, Fix…
  - Give as much context as necessary and as little as possible
  - Open it as a draft if it’s a work in progress
-->

### WHY are these changes introduced?

This PR introduces a `hidden` property to `FilterInterface`, allowing to
programmatically add extra filters to `IndexFilter`, while not making
them accessible through the dropdown

### WHAT is this pull request doing?

<!--
  Summary of the changes committed.

Before / after screenshots are appreciated for UI changes. Make sure to
include alt text that describes the screenshot.

  Include a video if your changes include interactive content.

If you include an animated gif showing your change, wrapping it in a
details tag is recommended. Gifs usually autoplay, which can cause
accessibility issues for people reviewing your PR:

  <details>
    <summary>Summary of your gif(s)</summary>
    <img src="..." alt="Description of what the gif shows">
  </details>
-->

### How to 🎩

🖥 [Local development
instructions](https://github.com/Shopify/polaris/blob/main/README.md#install-dependencies-and-build-workspaces)
🗒 [General tophatting
guidelines](https://github.com/Shopify/polaris/blob/main/documentation/Tophatting.md)
📄 [Changelog
guidelines](https://github.com/Shopify/polaris/blob/main/.github/CONTRIBUTING.md#changelog)

### 🎩 checklist

- [x] Tested a
[snapshot](https://github.com/Shopify/polaris/blob/main/documentation/Releasing.md#-snapshot-releases)
- [x] Tested on
[mobile](https://github.com/Shopify/polaris/blob/main/documentation/Tophatting.md#cross-browser-testing)
- [x] Tested on [multiple
browsers](https://help.shopify.com/en/manual/shopify-admin/supported-browsers)
- [x] Tested for
[accessibility](https://github.com/Shopify/polaris/blob/main/documentation/Accessibility%20testing.md)
- [x] Updated the component's `README.md` with documentation changes
- [x] [Tophatted
documentation](https://github.com/Shopify/polaris/blob/main/documentation/Tophatting%20documentation.md)
changes in the style guide

---------

Co-authored-by: Chloe Rice <chloerice@users.noreply.github.com>
  • Loading branch information
m4thieulavoie and chloerice authored Dec 14, 2023
1 parent 1d20e1b commit bc4272a
Show file tree
Hide file tree
Showing 6 changed files with 720 additions and 1 deletion.
6 changes: 6 additions & 0 deletions .changeset/olive-pans-tickle.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@shopify/polaris': patch
'polaris.shopify.com': patch
---

Added an optional `hidden` property to the `Filters` `FilterInterface` type to support `filters` that are only set programmatically
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,7 @@ export function FiltersBar({
);

const unsectionedFilters = unpinnedFilters
.filter((filter) => !filter.section)
.filter((filter) => !filter.section && !filter.hidden)
.map(filterToActionItem);

const sectionedFilters = unpinnedFilters
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,50 @@ describe('<FiltersBar />', () => {
});
});

it('does not render hidden filters in the ActionList', () => {
const scrollSpy = jest.fn();
HTMLElement.prototype.scroll = scrollSpy;
const props: FiltersBarProps = {
...defaultProps,
filters: [
...defaultProps.filters,
{
key: 'hiddenField',
label: 'Hidden field',
filter: null,
hidden: true,
},
],
};
const wrapper = mountWithApp(<FiltersBar {...props} />);

wrapper.act(() => {
wrapper
.find('button', {
'aria-label': 'Add filter',
})!
.trigger('onClick');
});

expect(wrapper).toContainReactComponent(ActionList, {
items: [
expect.objectContaining({content: defaultProps.filters[0].label}),
expect.objectContaining({content: defaultProps.filters[2].label}),
],
});
expect(wrapper.find(ActionList)).not.toHaveReactProps({
items: expect.arrayContaining([
{
key: 'hiddenField',
label: 'Hidden field',
filter: null,
content: 'Hidden field',
onAction: expect.any(Function),
},
]),
});
});

it('renders the unpinned disabled filters inside a Popover', () => {
const scrollSpy = jest.fn();
HTMLElement.prototype.scroll = scrollSpy;
Expand Down
298 changes: 298 additions & 0 deletions polaris-react/src/components/IndexFilters/IndexFilters.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1001,6 +1001,304 @@ export function WithPrefilledFilters() {
}
}

export function WithHiddenFilter() {
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<string[] | null>([
'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: (
<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}
/>
),
},
{
key: 'hiddenFilter',
label: 'Filter not accessible from the dropdown',
hidden: true,
filter: null,
},
];

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,
});
}

appliedFilters.push({
key: 'hiddenFilter',
label: 'Filter not accessible from the dropdown',
onRemove: handleTaggedWithRemove,
});

return (
<Card padding="0">
<IndexFilters
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;
}
}
}

export function WithAsyncData() {
const sleep = (ms: number) =>
new Promise((resolve) => setTimeout(resolve, ms));
Expand Down
2 changes: 2 additions & 0 deletions polaris-react/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -401,6 +401,8 @@ export interface FilterInterface {
suffix?: React.ReactNode;
/** Optional section heading that this filter will go under */
section?: string;
/** Optional: hides the filter from the dropdown */
hidden?: boolean;
}

/* Useful for defining mutually exclusive props such as:
Expand Down
Loading

0 comments on commit bc4272a

Please sign in to comment.