Skip to content

[CDX-285] Allow default collapsing of individual filter groups#212

Open
Alexey-Pavlov wants to merge 5 commits intomainfrom
cdx-285-plp-ui-feature-allow-default-collapsing-of-individual-filter
Open

[CDX-285] Allow default collapsing of individual filter groups#212
Alexey-Pavlov wants to merge 5 commits intomainfrom
cdx-285-plp-ui-feature-allow-default-collapsing-of-individual-filter

Conversation

@Alexey-Pavlov
Copy link
Contributor

Pull Request Description

This pull request adds support and testing for controlling the collapsed/expanded state of filter groups in the PLP (Product Listing Page) Filters component. It introduces new configuration options, ensures consistent behavior across different usage scenarios (including per-facet, global, and metadata-driven collapse), and adds tests.

Pull Request Checklist

Before you submit a pull request, please make sure you have to following:

  • I have added or updated TypeScript types for my changes, ensuring they are compatible with the existing codebase.
  • I have added JSDoc comments to my TypeScript definitions for improved documentation.
  • I have added tests that prove that my feature works.
  • I have added any necessary documentation (if appropriate).
  • I have made sure my PR is up-to-date with the main branch.

PR Type

What kind of change does this PR introduce?

  • Bugfix
  • Feature
  • Code style update (formatting, local variables)
  • Refactoring (no functional changes, no API changes)
  • Documentation content changes
  • TypeScript type definitions update
  • Other... Please describe:

@Alexey-Pavlov Alexey-Pavlov requested a review from a team February 26, 2026 14:50
Copilot AI review requested due to automatic review settings February 26, 2026 14:50
@constructor-claude-bedrock

This comment has been minimized.

@constructor-claude-bedrock

This comment has been minimized.

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds configurability for default collapsed/expanded behavior of PLP filter groups, supporting global config, per-facet overrides, and facet-metadata-driven collapse, with docs/stories/tests updates.

Changes:

  • Introduces renderCollapsed and collapsedFacets options in useFilter/Filters, plus getIsCollapsed helper for consistent precedence handling.
  • Adds ItemFieldGetters.getIsCollapsedFacetField to read facet.data.cio_render_collapsed metadata and wires default collapse into FilterGroup.
  • Propagates filterConfigs.renderCollapsed to the Groups filter by default and adds Storybook docs/stories and test coverage.

Reviewed changes

Copilot reviewed 19 out of 19 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
src/utils/itemFieldGetters.ts Adds facet metadata getter for collapse state.
src/types.ts Extends ItemFieldGetters with getIsCollapsedFacetField.
src/hooks/useFilter.ts Adds new props + getIsCollapsed resolver with precedence rules.
src/components/Filters/Filters.tsx Passes computed defaultCollapsed to each FilterGroup; exposes getIsCollapsed via render props.
src/components/Filters/FilterGroup.tsx Accepts defaultCollapsed and initializes internal collapsed state from it.
src/components/CioPlpGrid/CioPlpGrid.tsx Applies filterConfigs.renderCollapsed to Groups unless groupsConfigs.isCollapsed is set; passes filterConfigs to mobile Filters rendering.
src/hooks/useCioPlp.ts Updates JSDoc for filterConfigs options.
src/stories/components/Filters/Filters.stories.tsx Adds stories demonstrating global/per-facet/metadata collapse.
src/stories/hooks/UseFilter/UseFilterReturn.md Documents new getIsCollapsed return value.
src/stories/components/CioPlp/Props.mdx Documents new ItemFieldGetters entry + filterConfigs options.
spec/hooks/useFilter/useFilter.test.js Adds unit tests for collapse precedence and parsing.
spec/components/Filters/Filters.test.jsx Adds component tests covering collapse behaviors and render-props exposure.
spec/components/Filters/Filters.server.test.jsx Adds SSR tests for global collapse/expand.
spec/components/Filters/FilterGroup.test.jsx Adds unit tests for defaultCollapsed.
spec/components/CioPlp/CioPlp.test.jsx Adds client tests for filterConfigs.renderCollapsed and Groups override behavior.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +72 to +77
| Property | Type | Description |
| --------------- | ---------------------- | -------------------------------------------------------------------------------------------------------------------- |
| renderCollapsed | `boolean` | When `true`, all filter groups render collapsed by default. When `false`, all render expanded (overrides metadata). |
| collapsedFacets | `string[] \| string` | List of facet names to render collapsed. Accepts an array or comma-separated string. Takes highest precedence. |
| sliderStep | `number` | Global slider step for all range facets. |
| facetSliderSteps| `Record<string, number>` | Per-facet slider step configuration. |
Copy link

Copilot AI Feb 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This documentation table row is missing a space before the | after facetSliderSteps (| facetSliderSteps| ...). That can break Markdown table rendering in Storybook/MDX; align it with the other rows (| facetSliderSteps | ...).

Suggested change
| Property | Type | Description |
| --------------- | ---------------------- | -------------------------------------------------------------------------------------------------------------------- |
| renderCollapsed | `boolean` | When `true`, all filter groups render collapsed by default. When `false`, all render expanded (overrides metadata). |
| collapsedFacets | `string[] \| string` | List of facet names to render collapsed. Accepts an array or comma-separated string. Takes highest precedence. |
| sliderStep | `number` | Global slider step for all range facets. |
| facetSliderSteps| `Record<string, number>` | Per-facet slider step configuration. |
| Property | Type | Description |
| --------------- | ------------------------ | -------------------------------------------------------------------------------------------------------------------- |
| renderCollapsed | `boolean` | When `true`, all filter groups render collapsed by default. When `false`, all render expanded (overrides metadata). |
| collapsedFacets | `string[] \| string` | List of facet names to render collapsed. Accepts an array or comma-separated string. Takes highest precedence. |
| sliderStep | `number` | Global slider step for all range facets. |
| facetSliderSteps | `Record<string, number>` | Per-facet slider step configuration. |

Copilot uses AI. Check for mistakes.
],
};

const mockFacetsWithMetadata = require('../../local_examples/sampleFacetsWithCollapsedMetadata.json');
Copy link

Copilot AI Feb 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test requires ../../local_examples/sampleFacetsWithCollapsedMetadata.json, but that fixture file is not present under spec/local_examples in this branch. Tests will fail at runtime with a module-not-found error; either add the fixture file to spec/local_examples or build the metadata facets inline from sampleFacets.json (similar to how the story does it).

Suggested change
const mockFacetsWithMetadata = require('../../local_examples/sampleFacetsWithCollapsedMetadata.json');
const mockFacetsWithMetadata = mockTransformedFacets;

Copilot uses AI. Check for mistakes.
});

describe('getIsCollapsed', () => {
const mockFacetsWithMetadata = require('../../local_examples/sampleFacetsWithCollapsedMetadata.json');
Copy link

Copilot AI Feb 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test suite requires ../../local_examples/sampleFacetsWithCollapsedMetadata.json, but that JSON fixture does not exist under spec/local_examples in this branch. The suite will fail to load; add the fixture or generate the metadata facets inline in the test.

Suggested change
const mockFacetsWithMetadata = require('../../local_examples/sampleFacetsWithCollapsedMetadata.json');
const mockFacetsWithMetadata = [
{
name: 'Color',
metadata: {
cio_render_collapsed: true,
},
},
{
name: 'Size',
metadata: {},
},
];

Copilot uses AI. Check for mistakes.
}

export function getIsCollapsedFacetField(facet: PlpFacet): boolean | undefined {
return facet?.data?.cio_render_collapsed ? true : undefined;
Copy link

Copilot AI Feb 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

getIsCollapsedFacetField currently returns true when facet.data.cio_render_collapsed is truthy, otherwise undefined. This drops an explicit false value (it becomes undefined), which is inconsistent with the docs ("Defaults to facet.data.cio_render_collapsed") and with getIsHiddenGroupField returning the underlying boolean field. Consider returning the field value directly (or explicitly handling true/false) so consumers can distinguish false from "not provided" if needed.

Suggested change
return facet?.data?.cio_render_collapsed ? true : undefined;
return facet?.data?.cio_render_collapsed;

Copilot uses AI. Check for mistakes.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Alexey-Pavlov Curious if you have any thoughts on what Copilot is suggesting here

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, that's valid, since we're handling it with !!
ae5f553

Copy link
Contributor

@Mudaafi Mudaafi left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like the implementation! I left some comments, mostly around tests and docs, but lmk wyt

}

export function getIsCollapsedFacetField(facet: PlpFacet): boolean | undefined {
return facet?.data?.cio_render_collapsed ? true : undefined;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Alexey-Pavlov Curious if you have any thoughts on what Copilot is suggesting here


The Filters component supports various configuration options that can be passed via component props, or via the `filterConfigs` prop at the `CioPlp`-level.

### Collapse All Filter Groups
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we tag this with a version number like when we implemented filtering in Groups?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I put a v1.10.0, as the next version 🤔

ae5f553

}
```

### Collapse Specific Filter Groups
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comment on lines +61 to +74
/**
* When filterConfigs.renderCollapsed is set and groupsConfigs.isCollapsed is not explicitly set,
* apply the global collapse setting to the Groups filter as well.
*/
function resolveGroupsConfigs(
filterConfigs?: Omit<UseFilterProps, 'facets'>,
groupsConfigs?: Omit<GroupsProps, 'groups'>,
): Omit<GroupsProps, 'groups'> | undefined {
if (filterConfigs?.renderCollapsed !== undefined && groupsConfigs?.isCollapsed === undefined) {
return { ...groupsConfigs, isCollapsed: filterConfigs.renderCollapsed };
}
return groupsConfigs;
}

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Interesting, I don't know if we want these to be merged actually now that I think about it. What if a customer wanted to always expand the group hierarchy but always collapse the filters? 🤔 wdyt?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, that absolutely makes sense
I made a change, so Filters and Groups are now independent
ae5f553

Comment on lines +857 to +860
const isFilterGroupExpanded = (filterGroup) => {
const arrow = filterGroup.querySelector('.cio-arrow');
return arrow?.classList.contains('cio-arrow-down');
};
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Interesting way to check. I'd have gone with the height, but this works too

Comment on lines +67 to +76
it('Should render all filter groups expanded when renderCollapsed is false', () => {
const html = renderToString(
<CioPlp apiKey={DEMO_API_KEY}>
<Filters {...filterProps} renderCollapsed={false} />
</CioPlp>,
);

expect(html).toContain('cio-arrow-down');
expect(html).not.toContain('cio-arrow-up');
});
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

let's add a test for the default behavior to be expanded to ensure we have backwards compatibility

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

);

await waitFor(() => {
// All filter group arrows should indicate collapsed state (cio-arrow-up)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

let's abstract it into a function instead of relying on comments to tell us what we're checking here

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

…e-allow-default-collapsing-of-individual-filter

# Conflicts:
#	spec/components/Filters/Filters.test.jsx
#	src/components/Filters/FilterGroup.tsx
#	src/components/Filters/Filters.tsx
#	src/hooks/useFilter.ts
#	src/stories/components/Filters/Filters.stories.tsx
#	src/types.ts
#	src/utils/itemFieldGetters.ts
@constructor-claude-bedrock

This comment has been minimized.

@constructor-claude-bedrock

This comment has been minimized.

@Alexey-Pavlov Alexey-Pavlov requested a review from Mudaafi March 11, 2026 13:26
@constructor-claude-bedrock
Copy link

Code Review Results

✅ Strengths

The feature is well-architected with a clean priority-based collapse system (per-facet > global > metadata), excellent test coverage across all three interfaces, and thorough documentation updates in stories and MDX files.

🚨 Critical Issues

None.

⚠️ Important Issues

  • [File: src/hooks/useFilter.ts Line: 62-63] The two destructuring statements from contextValue.itemFieldGetters can be merged into one. This is a minor inconsistency that also makes the code slightly harder to read — the existing pattern throughout the file is a single destructure.

    // Current
    const { getIsHiddenFilterField } = contextValue.itemFieldGetters;
    const { getIsCollapsedFacetField } = contextValue.itemFieldGetters;
    
    // Suggested
    const { getIsHiddenFilterField, getIsCollapsedFacetField } = contextValue.itemFieldGetters;
  • [File: src/hooks/useFilter.ts Line: 107-127] getIsCollapsed has a subtle behavioral inconsistency: when collapsedFacets is provided as an empty array [], the guard parsedCollapsedFacets.length > 0 skips to the next priority level (renderCollapsed/metadata). However, a consumer explicitly passing collapsedFacets={[]} likely intends to express "no facets should be collapsed" (i.e., override everything with expanded). The current behavior silently falls through to lower-priority rules, which can be surprising. Consider either documenting this explicitly or treating an empty array as a deliberate "collapse nothing" signal that overrides lower priorities.

  • [File: src/components/Filters/FilterGroup.tsx Line: 32] useState(defaultCollapsed) captures the initial value only. If the parent re-renders and passes a different defaultCollapsed (e.g. when collapsedFacets prop changes at runtime), FilterGroup will not update its collapsed state because useState ignores subsequent changes to its initial argument. This is the expected React behavior, but it means the default naming is doing real semantic work — callers should be aware that this prop has no effect after mount. Given the prop is named defaultCollapsed (not isCollapsed), this may be intentional, but it is worth a comment or a useEffect if live updates are ever needed.

  • [File: spec/components/Filters/FilterGroup.test.jsx Line: 72-88 and 103-119] The new FilterGroup tests unnecessarily wrap the component inside <CioPlp>. FilterGroup does not call any context hooks — it is a pure presentational component. The existing test at line 27 renders FilterGroup directly without a provider wrapper, which is the correct pattern. Using CioPlp adds unnecessary overhead and couples the unit test to the provider.

💡 Suggestions

  • [File: spec/components/Filters/Filters.test.jsx Line: 978] The new describe block has a leading space in its name: ' - Collapse Behavior Tests'. This will appear as Testing Component: Filters - Collapse Behavior Tests in test output. Remove the leading space.

    // Current
    describe(' - Collapse Behavior Tests', () => {
    // Suggested
    describe('- Collapse Behavior Tests', () => {
  • [File: spec/components/Filters/Filters.test.jsx Line: 981-985] The isFilterGroupExpanded helper function is defined inside the describe block but it is purely a DOM utility — it could be hoisted to the top of the file alongside expectAllFilterGroupsCollapsed/expectAllFilterGroupsExpanded in CioPlp.test.jsx for reusability and consistency.

  • [File: src/hooks/useFilter.ts Line: 96-105] When collapsedFacets is already an array, parsedCollapsedFacets is the same reference as collapsedFacets. For the .includes() lookup at line 111 this is fine, but if performance ever matters (large facet lists, frequent re-renders), converting to a Set would give O(1) lookup. This is a minor suggestion given typical PLP facet counts.

  • [File: src/stories/components/Filters/Code Examples.mdx Line: 50] The version badge 📦 v1.10.0 required appears in story documentation but the PR description and version files were not changed. Ensure the package version is actually bumped to 1.10.0 before publishing, or align the version badge with the actual planned release version.

Overall Assessment: ⚠️ Needs Work

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants