Skip to content

Commit 4ee54f3

Browse files
FCT 1187 - enable filter selection in filters (#2971)
* feat(filter selects): use new appearance:filter and optionStyle:checkbox props for select input * feat(filters): [publish_preview] insure that options and option groups are handled by the 'add filters' select input * chore(rebase): update yarn.lock * feat(filters): add aria-labels to fixture inputs for better ability to find them in tests, add aria-label to ul containing filter chips in TriggerButton for a11y/easier test targeting, add more complete tests for Footer, TriggerButton, and FilterMenu * test(filters): add tests * feat(filters): add aria-live=polite to filtersList container and selected filter values containers * feat(filters): add aria-label to add filter select in filters, use radix dialog to find filter inputs instead of looking for combobox in filters.spec.tsx * feat(filter selects): use new appearance:filter and optionStyle:checkbox props for select input * fix(deps): add proptypes back in * feat(filter selects): use new appearance:filter and optionStyle:checkbox props for select input * feat(filters stories): clean up story code a bit * feat(filters stories): rebase and clean up readmes/icons * feat(filters): update visible filters if number of persisted filters changes * feat(filters): remove todo comments from jsdoc blocks in filters * feat(filters): update filter-menu to apply custom styling to select wrappers when parent of custom select menu to allow the select input's options menu to scroll, add max-height to filter-menu dropdowns, update usage example to be more readable and add comments, update story to open filters by default so user doesn't have to click on the filters button to see the list, add filters-with-state fixture for use in VRT, add defaultOpen prop to filters so that the filters list can mount on first render, add VRTs for various open/closed states * feat(filters): add changeset, update readme with more helpful description * FCT-1187 - Filter Selection tests (#2979) * feat(filters): implement filters selection tests, add test helpers * feat(vrt): only defaultOpen a filterMenu if it isnt persisted and there are more locally visible filters than there are visible filters from props --------- Co-authored-by: Byron Wall <[email protected]> * chore(filters): restore spec files * fix(add filter button): add role of button to wrapping div that receives forwarded ref from radix trigger popover * fix(filters divider): divider is 1px * feat(filter selects): use new appearance:filter and optionStyle:checkbox props for select input * fix(add filter button): remove button role, since wrapper is wrapping a button * feat(operators): add new filter.operatorLabel type/prop to handle display of the selected operator value in the trigger menu, add test to ensure operator label displays in trigger button, make radio option text 14px --------- Co-authored-by: Valorie <[email protected]> Co-authored-by: Valorie <[email protected]>
1 parent f723065 commit 4ee54f3

30 files changed

+2540
-1232
lines changed

.changeset/quick-icons-act.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@commercetools-uikit/filters': minor
3+
---
4+
5+
Adds filters component to UI Kit

packages/components/filters/README.md

Lines changed: 194 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,11 @@
55

66
## Description
77

8-
The `Filters` component displays all active filters. It also allows for adding and removing filters.
8+
The `Filters` component pattern is a component that provides the controls to filter the items in a table or list.
99

10-
This description is a stub and shold be expanded as development continues.
10+
- Available filter controls are configured in the `filters` array. Each `filter` object includes a `filterMenuConfiguration` object, in which the inputs that allow the user to select a value for that filter are defined.
11+
12+
- The selected values for each filter are passed to the component in the `appliedFilters` array. Values in the `appliedFilters` array will be displayed in each filter's menu button.
1113

1214
## Installation
1315

@@ -34,14 +36,197 @@ npm --save install react
3436
```jsx
3537
import Filters from '@commercetools-uikit/filters';
3638

37-
/**TODO: EXPAND THIS */
38-
const Example = () => <Filters />;
39-
40-
export default Example;
39+
/** Input for a specific filter, provided by parent application */
40+
import FirstFilterInput from 'path/to/first/filter/input'; // eslint-disable-line import/no-unresolved
41+
42+
/** Input for search query, provided by parent application */
43+
import SearchInput from 'path/to/search/input'; // eslint-disable-line import/no-unresolved
44+
45+
/** Input for a specific filter, provided by parent application */
46+
import SecondFilterInput from 'path/to/second/filter/input'; // eslint-disable-line import/no-unresolved
47+
48+
/** Filter and search state, provided by parent application (does not have to be hook)
49+
see storybook code block for more in depth example of simple state management */
50+
import useFilterState from 'path/to/your/filter/state'; // eslint-disable-line import/no-unresolved
51+
52+
const FiltersExample = () => {
53+
const {
54+
// change handler for FirstFilterInput
55+
onFirstFilterInputChange,
56+
// callback to clear FirstFilterInput value
57+
onClearFirstFilterInput,
58+
// value of FirstFilterInput
59+
firstFilterInputValue,
60+
// change handler for SecondFilterInput
61+
onSecondFilterInputChange,
62+
// callback to clear SecondFilterInput value
63+
onClearSecondFilterInput,
64+
// value of SecondFilterInput
65+
secondFilterInputValue,
66+
// callback to clear all filter inputs and selected values
67+
onClearAllFilters,
68+
// selected/applied values of all filters
69+
selectedFilterValues,
70+
} = useFilterState();
71+
72+
const filters = [
73+
{
74+
key: 'firstFilter',
75+
label: 'First Filter',
76+
filterMenuConfiguration: {
77+
renderMenuBody: () => (
78+
<FirstFilterInput
79+
onChange={onFirstFilterInputChange}
80+
value={firstFilterInputValue}
81+
/>
82+
),
83+
onClearRequest: onClearFirstFilterInput,
84+
},
85+
},
86+
{
87+
key: 'secondFilter',
88+
label: 'Second Filter',
89+
filterMenuConfiguration: {
90+
renderMenuBody: () => (
91+
<SecondFilterInput
92+
onChange={onSecondFilterInputChange}
93+
value={secondFilterInputValue}
94+
/>
95+
),
96+
onClearRequest: onClearSecondFilterInput,
97+
},
98+
},
99+
];
100+
101+
return (
102+
<Filters
103+
appliedFilters={selectedFilterValues}
104+
filters={filters}
105+
onClearAllRequest={onClearAllFilters}
106+
renderSearchComponent={<SearchInput />}
107+
/>
108+
);
109+
};
110+
111+
export default FiltersExample;
41112
```
42113

43114
## Properties
44115

45-
| Props | Type | Required | Default | Description |
46-
| ------- | -------- | :------: | ------- | ------------------- |
47-
| `label` | `string` | | | This is a stub prop |
116+
| Props | Type | Required | Default | Description |
117+
| ----------------------- | ---------------------------------------------------------------------------------- | :------: | ------- | ------------------------------------------------------------------------------------------------------------------------ |
118+
| `appliedFilters` | `Array: TAppliedFilter[]`<br/>[See signature.](#signature-appliedFilters) || | array of applied filters, each containing a unique key and an array of values. |
119+
| `filters` | `Array: TFilterConfiguration[]`<br/>[See signature.](#signature-filters) || | configuration for the available filters. |
120+
| `filterGroups` | `Array: TFilterGroupConfiguration[]`<br/>[See signature.](#signature-filterGroups) | | | optional configuration for filter groups. |
121+
| `onClearAllRequest` | `Function`<br/>[See signature.](#signature-onClearAllRequest) || | controls the `clear all` (added filters) button from the menu list, meant to clear the parent application's filter state |
122+
| `onAddFilterRequest` | `Function`<br/>[See signature.](#signature-onAddFilterRequest) | | | optional callback when the add filter button is clicked |
123+
| `renderSearchComponent` | `ReactNode` || | function to render a search input, selectable from applicable UI Kit components. |
124+
| `defaultOpen` | `boolean` | | `false` | controls whether the filters list is initially open |
125+
126+
## Signatures
127+
128+
### Signature `appliedFilters`
129+
130+
```ts
131+
{
132+
/**
133+
* unique identifier for the filter
134+
*/
135+
filterKey: string;
136+
/**
137+
* the values applied to this filter by the user
138+
*/
139+
values: TAppliedFilterValue[];
140+
}
141+
```
142+
143+
### Signature `filters`
144+
145+
```ts
146+
{
147+
/**
148+
* unique identifier for the filter
149+
*/
150+
key: string;
151+
/**
152+
* formatted message to display the filter name
153+
*/
154+
label: ReactNode;
155+
/**
156+
* formatted message to display the selected operator value
157+
*/
158+
operatorLabel?: ReactNode;
159+
/**
160+
* configuration object for the filter menu.
161+
*/
162+
filterMenuConfiguration: {
163+
/**
164+
* the input in which the user selects values for the filter
165+
*/
166+
renderMenuBody: () => ReactNode;
167+
/**
168+
* the input in which the user can select which operator should be used for this filter
169+
*/
170+
renderOperatorsInput?: () => ReactNode;
171+
/**
172+
* optional button that allows the user to apply selected filter values
173+
*/
174+
renderApplyButton?: () => ReactNode;
175+
/**
176+
* controls whether `clear` button in Menu Body Footer is displayed
177+
*/
178+
onClearRequest: (
179+
event?: MouseEvent<HTMLButtonElement> | KeyboardEvent<HTMLButtonElement>
180+
) => void;
181+
/**
182+
* controls whether `sort` button in Menu Body Header is displayed
183+
*/
184+
onSortRequest?: (
185+
event?: MouseEvent<HTMLButtonElement> | KeyboardEvent<HTMLButtonElement>
186+
) => void;
187+
};
188+
/**
189+
* optional key to group filters together.
190+
*/
191+
groupKey?: string;
192+
/**
193+
* indicates whether filter menu can be removed from filters
194+
*/
195+
isPersistent?: boolean;
196+
/**
197+
* indicates whether the filter is disabled
198+
*/
199+
isDisabled?: boolean;
200+
}
201+
```
202+
203+
### Signature `filterGroups`
204+
205+
```ts
206+
{
207+
/**
208+
* unique identifier for the filter group
209+
*/
210+
key: string;
211+
/**
212+
* formatted message to display the filter group name
213+
*/
214+
label: ReactNode;
215+
}
216+
```
217+
218+
### Signature `onClearAllRequest`
219+
220+
```ts
221+
(
222+
event?: MouseEvent<HTMLButtonElement> | KeyboardEvent<HTMLButtonElement>
223+
) => void
224+
```
225+
226+
### Signature `onAddFilterRequest`
227+
228+
```ts
229+
(
230+
event?: MouseEvent<HTMLButtonElement> | KeyboardEvent<HTMLButtonElement>
231+
) => void
232+
```
Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1-
The `Filters` component displays all active filters. It also allows for adding and removing filters.
1+
The `Filters` component pattern is a component that provides the controls to filter the items in a table or list.
22

3-
This description is a stub and shold be expanded as development continues.
3+
- Available filter controls are configured in the `filters` array. Each `filter` object includes a `filterMenuConfiguration` object, in which the inputs that allow the user to select a value for that filter are defined.
4+
5+
- The selected values for each filter are passed to the component in the `appliedFilters` array. Values in the `appliedFilters` array will be displayed in each filter's menu button.
Lines changed: 72 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,75 @@
11
import Filters from '@commercetools-uikit/filters';
22

3-
/**TODO: EXPAND THIS */
4-
const Example = () => <Filters />;
3+
/** Input for a specific filter, provided by parent application */
4+
import FirstFilterInput from 'path/to/first/filter/input'; // eslint-disable-line import/no-unresolved
55

6-
export default Example;
6+
/** Input for search query, provided by parent application */
7+
import SearchInput from 'path/to/search/input'; // eslint-disable-line import/no-unresolved
8+
9+
/** Input for a specific filter, provided by parent application */
10+
import SecondFilterInput from 'path/to/second/filter/input'; // eslint-disable-line import/no-unresolved
11+
12+
/** Filter and search state, provided by parent application (does not have to be hook)
13+
see storybook code block for more in depth example of simple state management */
14+
import useFilterState from 'path/to/your/filter/state'; // eslint-disable-line import/no-unresolved
15+
16+
const FiltersExample = () => {
17+
const {
18+
// change handler for FirstFilterInput
19+
onFirstFilterInputChange,
20+
// callback to clear FirstFilterInput value
21+
onClearFirstFilterInput,
22+
// value of FirstFilterInput
23+
firstFilterInputValue,
24+
// change handler for SecondFilterInput
25+
onSecondFilterInputChange,
26+
// callback to clear SecondFilterInput value
27+
onClearSecondFilterInput,
28+
// value of SecondFilterInput
29+
secondFilterInputValue,
30+
// callback to clear all filter inputs and selected values
31+
onClearAllFilters,
32+
// selected/applied values of all filters
33+
selectedFilterValues,
34+
} = useFilterState();
35+
36+
const filters = [
37+
{
38+
key: 'firstFilter',
39+
label: 'First Filter',
40+
filterMenuConfiguration: {
41+
renderMenuBody: () => (
42+
<FirstFilterInput
43+
onChange={onFirstFilterInputChange}
44+
value={firstFilterInputValue}
45+
/>
46+
),
47+
onClearRequest: onClearFirstFilterInput,
48+
},
49+
},
50+
{
51+
key: 'secondFilter',
52+
label: 'Second Filter',
53+
filterMenuConfiguration: {
54+
renderMenuBody: () => (
55+
<SecondFilterInput
56+
onChange={onSecondFilterInputChange}
57+
value={secondFilterInputValue}
58+
/>
59+
),
60+
onClearRequest: onClearSecondFilterInput,
61+
},
62+
},
63+
];
64+
65+
return (
66+
<Filters
67+
appliedFilters={selectedFilterValues}
68+
filters={filters}
69+
onClearAllRequest={onClearAllFilters}
70+
renderSearchComponent={<SearchInput />}
71+
/>
72+
);
73+
};
74+
75+
export default FiltersExample;

packages/components/filters/package.json

Lines changed: 13 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -21,23 +21,26 @@
2121
"dependencies": {
2222
"@babel/runtime": "^7.20.13",
2323
"@babel/runtime-corejs3": "^7.20.13",
24-
"@commercetools-uikit/collapsible-motion": "workspace:^",
25-
"@commercetools-uikit/design-system": "workspace:^",
26-
"@commercetools-uikit/dropdown-menu": "workspace:^",
27-
"@commercetools-uikit/flat-button": "workspace:^",
28-
"@commercetools-uikit/icon-button": "workspace:^",
29-
"@commercetools-uikit/icons": "workspace:^",
30-
"@commercetools-uikit/secondary-icon-button": "workspace:^",
31-
"@commercetools-uikit/select-input": "workspace:^",
32-
"@commercetools-uikit/spacings": "workspace:^",
33-
"@commercetools-uikit/utils": "workspace:^",
24+
"@commercetools-uikit/collapsible-motion": "19.15.0",
25+
"@commercetools-uikit/design-system": "19.15.0",
26+
"@commercetools-uikit/flat-button": "19.15.0",
27+
"@commercetools-uikit/icon-button": "19.15.0",
28+
"@commercetools-uikit/icons": "19.15.0",
29+
"@commercetools-uikit/secondary-icon-button": "19.15.0",
30+
"@commercetools-uikit/select-input": "19.15.0",
31+
"@commercetools-uikit/spacings": "19.15.0",
32+
"@commercetools-uikit/utils": "19.15.0",
3433
"@emotion/react": "^11.10.5",
3534
"@emotion/styled": "^11.10.5",
3635
"@radix-ui/react-popover": "^1.1.2",
3736
"prop-types": "15.8.1",
3837
"react-intl": "^6.3.2"
3938
},
4039
"devDependencies": {
40+
"@commercetools-uikit/primary-button": "workspace:^",
41+
"@commercetools-uikit/radio-input": "workspace:^",
42+
"@commercetools-uikit/search-text-input": "workspace:^",
43+
"@commercetools-uikit/text-input": "workspace:^",
4144
"react": "17.0.2"
4245
},
4346
"peerDependencies": {

packages/components/filters/src/badge/badge.spec.tsx

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,4 +11,18 @@ describe('Filters Badge', () => {
1111
const badge = await screen.findByRole('status');
1212
expect(badge.textContent).toEqual('+1');
1313
});
14+
15+
it('should render the badge with a custom aria-label attribute if provided', async () => {
16+
const ariaLabel = 'custom aria-label';
17+
18+
render(<Badge {...defaultProps} aria-label={ariaLabel} />);
19+
const badge = await screen.findByRole('status', { name: ariaLabel });
20+
expect(badge).toBeInTheDocument();
21+
});
22+
23+
it('should apply disabled styles when isDisabled is true', () => {
24+
render(<Badge {...defaultProps} isDisabled />);
25+
const badge = screen.getByRole('status');
26+
expect(badge.className).toMatch(/disabledBadgeStyles/i);
27+
});
1428
});

packages/components/filters/src/badge/badge.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,15 +37,15 @@ const badgeStyles = css`
3737
height: ${designTokens.spacing40};
3838
`;
3939

40-
const disabledBageStyles = css`
40+
const disabledBadgeStyles = css`
4141
background-color: ${designTokens.colorNeutral};
4242
`;
4343

4444
function Badge(props: TBadgeProps) {
4545
return (
4646
<span
4747
aria-label={props['aria-label']}
48-
css={[badgeStyles, props.isDisabled && disabledBageStyles]}
48+
css={[badgeStyles, props.isDisabled && disabledBadgeStyles]}
4949
id={props.id}
5050
role="status"
5151
>

0 commit comments

Comments
 (0)