Skip to content

Commit

Permalink
FCT 1187 - enable filter selection in filters (#2971)
Browse files Browse the repository at this point in the history
* 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]>
  • Loading branch information
3 people authored Nov 11, 2024
1 parent f723065 commit 4ee54f3
Show file tree
Hide file tree
Showing 30 changed files with 2,540 additions and 1,232 deletions.
5 changes: 5 additions & 0 deletions .changeset/quick-icons-act.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@commercetools-uikit/filters': minor
---

Adds filters component to UI Kit
203 changes: 194 additions & 9 deletions packages/components/filters/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,11 @@

## Description

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

This description is a stub and shold be expanded as development continues.
- 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.

- 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.

## Installation

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

/**TODO: EXPAND THIS */
const Example = () => <Filters />;

export default Example;
/** Input for a specific filter, provided by parent application */
import FirstFilterInput from 'path/to/first/filter/input'; // eslint-disable-line import/no-unresolved

/** Input for search query, provided by parent application */
import SearchInput from 'path/to/search/input'; // eslint-disable-line import/no-unresolved

/** Input for a specific filter, provided by parent application */
import SecondFilterInput from 'path/to/second/filter/input'; // eslint-disable-line import/no-unresolved

/** Filter and search state, provided by parent application (does not have to be hook)
see storybook code block for more in depth example of simple state management */
import useFilterState from 'path/to/your/filter/state'; // eslint-disable-line import/no-unresolved

const FiltersExample = () => {
const {
// change handler for FirstFilterInput
onFirstFilterInputChange,
// callback to clear FirstFilterInput value
onClearFirstFilterInput,
// value of FirstFilterInput
firstFilterInputValue,
// change handler for SecondFilterInput
onSecondFilterInputChange,
// callback to clear SecondFilterInput value
onClearSecondFilterInput,
// value of SecondFilterInput
secondFilterInputValue,
// callback to clear all filter inputs and selected values
onClearAllFilters,
// selected/applied values of all filters
selectedFilterValues,
} = useFilterState();

const filters = [
{
key: 'firstFilter',
label: 'First Filter',
filterMenuConfiguration: {
renderMenuBody: () => (
<FirstFilterInput
onChange={onFirstFilterInputChange}
value={firstFilterInputValue}
/>
),
onClearRequest: onClearFirstFilterInput,
},
},
{
key: 'secondFilter',
label: 'Second Filter',
filterMenuConfiguration: {
renderMenuBody: () => (
<SecondFilterInput
onChange={onSecondFilterInputChange}
value={secondFilterInputValue}
/>
),
onClearRequest: onClearSecondFilterInput,
},
},
];

return (
<Filters
appliedFilters={selectedFilterValues}
filters={filters}
onClearAllRequest={onClearAllFilters}
renderSearchComponent={<SearchInput />}
/>
);
};

export default FiltersExample;
```

## Properties

| Props | Type | Required | Default | Description |
| ------- | -------- | :------: | ------- | ------------------- |
| `label` | `string` | | | This is a stub prop |
| Props | Type | Required | Default | Description |
| ----------------------- | ---------------------------------------------------------------------------------- | :------: | ------- | ------------------------------------------------------------------------------------------------------------------------ |
| `appliedFilters` | `Array: TAppliedFilter[]`<br/>[See signature.](#signature-appliedFilters) || | array of applied filters, each containing a unique key and an array of values. |
| `filters` | `Array: TFilterConfiguration[]`<br/>[See signature.](#signature-filters) || | configuration for the available filters. |
| `filterGroups` | `Array: TFilterGroupConfiguration[]`<br/>[See signature.](#signature-filterGroups) | | | optional configuration for filter groups. |
| `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 |
| `onAddFilterRequest` | `Function`<br/>[See signature.](#signature-onAddFilterRequest) | | | optional callback when the add filter button is clicked |
| `renderSearchComponent` | `ReactNode` || | function to render a search input, selectable from applicable UI Kit components. |
| `defaultOpen` | `boolean` | | `false` | controls whether the filters list is initially open |

## Signatures

### Signature `appliedFilters`

```ts
{
/**
* unique identifier for the filter
*/
filterKey: string;
/**
* the values applied to this filter by the user
*/
values: TAppliedFilterValue[];
}
```

### Signature `filters`

```ts
{
/**
* unique identifier for the filter
*/
key: string;
/**
* formatted message to display the filter name
*/
label: ReactNode;
/**
* formatted message to display the selected operator value
*/
operatorLabel?: ReactNode;
/**
* configuration object for the filter menu.
*/
filterMenuConfiguration: {
/**
* the input in which the user selects values for the filter
*/
renderMenuBody: () => ReactNode;
/**
* the input in which the user can select which operator should be used for this filter
*/
renderOperatorsInput?: () => ReactNode;
/**
* optional button that allows the user to apply selected filter values
*/
renderApplyButton?: () => ReactNode;
/**
* controls whether `clear` button in Menu Body Footer is displayed
*/
onClearRequest: (
event?: MouseEvent<HTMLButtonElement> | KeyboardEvent<HTMLButtonElement>
) => void;
/**
* controls whether `sort` button in Menu Body Header is displayed
*/
onSortRequest?: (
event?: MouseEvent<HTMLButtonElement> | KeyboardEvent<HTMLButtonElement>
) => void;
};
/**
* optional key to group filters together.
*/
groupKey?: string;
/**
* indicates whether filter menu can be removed from filters
*/
isPersistent?: boolean;
/**
* indicates whether the filter is disabled
*/
isDisabled?: boolean;
}
```

### Signature `filterGroups`

```ts
{
/**
* unique identifier for the filter group
*/
key: string;
/**
* formatted message to display the filter group name
*/
label: ReactNode;
}
```

### Signature `onClearAllRequest`

```ts
(
event?: MouseEvent<HTMLButtonElement> | KeyboardEvent<HTMLButtonElement>
) => void
```

### Signature `onAddFilterRequest`

```ts
(
event?: MouseEvent<HTMLButtonElement> | KeyboardEvent<HTMLButtonElement>
) => void
```
6 changes: 4 additions & 2 deletions packages/components/filters/docs/description.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
The `Filters` component displays all active filters. It also allows for adding and removing filters.
The `Filters` component pattern is a component that provides the controls to filter the items in a table or list.

This description is a stub and shold be expanded as development continues.
- 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.

- 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.
75 changes: 72 additions & 3 deletions packages/components/filters/docs/usage-example.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,75 @@
import Filters from '@commercetools-uikit/filters';

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

export default Example;
/** Input for search query, provided by parent application */
import SearchInput from 'path/to/search/input'; // eslint-disable-line import/no-unresolved

/** Input for a specific filter, provided by parent application */
import SecondFilterInput from 'path/to/second/filter/input'; // eslint-disable-line import/no-unresolved

/** Filter and search state, provided by parent application (does not have to be hook)
see storybook code block for more in depth example of simple state management */
import useFilterState from 'path/to/your/filter/state'; // eslint-disable-line import/no-unresolved

const FiltersExample = () => {
const {
// change handler for FirstFilterInput
onFirstFilterInputChange,
// callback to clear FirstFilterInput value
onClearFirstFilterInput,
// value of FirstFilterInput
firstFilterInputValue,
// change handler for SecondFilterInput
onSecondFilterInputChange,
// callback to clear SecondFilterInput value
onClearSecondFilterInput,
// value of SecondFilterInput
secondFilterInputValue,
// callback to clear all filter inputs and selected values
onClearAllFilters,
// selected/applied values of all filters
selectedFilterValues,
} = useFilterState();

const filters = [
{
key: 'firstFilter',
label: 'First Filter',
filterMenuConfiguration: {
renderMenuBody: () => (
<FirstFilterInput
onChange={onFirstFilterInputChange}
value={firstFilterInputValue}
/>
),
onClearRequest: onClearFirstFilterInput,
},
},
{
key: 'secondFilter',
label: 'Second Filter',
filterMenuConfiguration: {
renderMenuBody: () => (
<SecondFilterInput
onChange={onSecondFilterInputChange}
value={secondFilterInputValue}
/>
),
onClearRequest: onClearSecondFilterInput,
},
},
];

return (
<Filters
appliedFilters={selectedFilterValues}
filters={filters}
onClearAllRequest={onClearAllFilters}
renderSearchComponent={<SearchInput />}
/>
);
};

export default FiltersExample;
23 changes: 13 additions & 10 deletions packages/components/filters/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,23 +21,26 @@
"dependencies": {
"@babel/runtime": "^7.20.13",
"@babel/runtime-corejs3": "^7.20.13",
"@commercetools-uikit/collapsible-motion": "workspace:^",
"@commercetools-uikit/design-system": "workspace:^",
"@commercetools-uikit/dropdown-menu": "workspace:^",
"@commercetools-uikit/flat-button": "workspace:^",
"@commercetools-uikit/icon-button": "workspace:^",
"@commercetools-uikit/icons": "workspace:^",
"@commercetools-uikit/secondary-icon-button": "workspace:^",
"@commercetools-uikit/select-input": "workspace:^",
"@commercetools-uikit/spacings": "workspace:^",
"@commercetools-uikit/utils": "workspace:^",
"@commercetools-uikit/collapsible-motion": "19.15.0",
"@commercetools-uikit/design-system": "19.15.0",
"@commercetools-uikit/flat-button": "19.15.0",
"@commercetools-uikit/icon-button": "19.15.0",
"@commercetools-uikit/icons": "19.15.0",
"@commercetools-uikit/secondary-icon-button": "19.15.0",
"@commercetools-uikit/select-input": "19.15.0",
"@commercetools-uikit/spacings": "19.15.0",
"@commercetools-uikit/utils": "19.15.0",
"@emotion/react": "^11.10.5",
"@emotion/styled": "^11.10.5",
"@radix-ui/react-popover": "^1.1.2",
"prop-types": "15.8.1",
"react-intl": "^6.3.2"
},
"devDependencies": {
"@commercetools-uikit/primary-button": "workspace:^",
"@commercetools-uikit/radio-input": "workspace:^",
"@commercetools-uikit/search-text-input": "workspace:^",
"@commercetools-uikit/text-input": "workspace:^",
"react": "17.0.2"
},
"peerDependencies": {
Expand Down
14 changes: 14 additions & 0 deletions packages/components/filters/src/badge/badge.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,18 @@ describe('Filters Badge', () => {
const badge = await screen.findByRole('status');
expect(badge.textContent).toEqual('+1');
});

it('should render the badge with a custom aria-label attribute if provided', async () => {
const ariaLabel = 'custom aria-label';

render(<Badge {...defaultProps} aria-label={ariaLabel} />);
const badge = await screen.findByRole('status', { name: ariaLabel });
expect(badge).toBeInTheDocument();
});

it('should apply disabled styles when isDisabled is true', () => {
render(<Badge {...defaultProps} isDisabled />);
const badge = screen.getByRole('status');
expect(badge.className).toMatch(/disabledBadgeStyles/i);
});
});
4 changes: 2 additions & 2 deletions packages/components/filters/src/badge/badge.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,15 +37,15 @@ const badgeStyles = css`
height: ${designTokens.spacing40};
`;

const disabledBageStyles = css`
const disabledBadgeStyles = css`
background-color: ${designTokens.colorNeutral};
`;

function Badge(props: TBadgeProps) {
return (
<span
aria-label={props['aria-label']}
css={[badgeStyles, props.isDisabled && disabledBageStyles]}
css={[badgeStyles, props.isDisabled && disabledBadgeStyles]}
id={props.id}
role="status"
>
Expand Down
Loading

0 comments on commit 4ee54f3

Please sign in to comment.