From 92bf79b9b0d0ea2972fc1a0ecf1c04f04e6a1729 Mon Sep 17 00:00:00 2001 From: Enes Kutay SEZEN Date: Wed, 28 Jan 2026 13:41:38 +0300 Subject: [PATCH 1/7] Allow hiding facet groups and options --- spec/components/Filters/Filters.test.jsx | 123 ++++++++++++++ spec/hooks/useFilter/useFilter.test.js | 153 ++++++++++++++++++ src/components/Filters/FilterGroup.tsx | 17 +- src/components/Filters/Filters.tsx | 10 +- .../Filters/UseFilterOptionsList.tsx | 22 ++- src/hooks/useFilter.ts | 24 ++- .../components/Filters/Code Examples.mdx | 77 +++++++++ .../components/Filters/Filters.stories.tsx | 43 +++++ src/types.ts | 2 + src/utils/itemFieldGetters.ts | 18 ++- 10 files changed, 480 insertions(+), 9 deletions(-) diff --git a/spec/components/Filters/Filters.test.jsx b/spec/components/Filters/Filters.test.jsx index f81e8241..1fbd27d2 100644 --- a/spec/components/Filters/Filters.test.jsx +++ b/spec/components/Filters/Filters.test.jsx @@ -753,4 +753,127 @@ describe('Testing Component: Filters', () => { }); }); }); + + describe(' - Facet Blacklisting Tests', () => { + it('Should not render facets when isHiddenFacetFn returns true', async () => { + const isHiddenFacetFn = (facet) => facet.name === 'color'; // lowercase 'color' + const { queryByText, getByText } = render( + + + , + ); + + await waitFor(() => { + // Color facet should not be rendered + expect(queryByText('Color')).not.toBeInTheDocument(); + // Other facets should still render + const otherFacet = mockTransformedFacets.find((f) => f.name !== 'color'); + expect(getByText(otherFacet.displayName)).toBeInTheDocument(); + }); + }); + + it('Should not render facets with data.cio_plp_hidden = true', async () => { + const facetsWithHidden = mockTransformedFacets.map((facet, index) => ({ + ...facet, + data: { + ...facet.data, + cio_plp_hidden: index === 0, // Hide first facet + }, + })); + + const { queryByText, getByText } = render( + + + , + ); + + await waitFor(() => { + // First facet should not be rendered + expect(queryByText(facetsWithHidden[0].displayName)).not.toBeInTheDocument(); + // Other facets should still render + expect(getByText(facetsWithHidden[1].displayName)).toBeInTheDocument(); + }); + }); + + it('Should not render options when isHiddenFacetOptionFn returns true', async () => { + const colorFacet = mockTransformedFacets.find((f) => f.name === 'color'); // lowercase + const optionToHide = colorFacet.options[0]; + const isHiddenFacetOptionFn = (option) => option.value === optionToHide.value; + + const { container } = render( + + + , + ); + + await waitFor(() => { + // The hidden option should not be rendered + const colorFilterGroup = container.querySelector('.cio-filter-group'); + const optionLabels = colorFilterGroup.querySelectorAll('.cio-filter-multiple-option label'); + const optionTexts = Array.from(optionLabels).map((label) => label.textContent); + + expect(optionTexts).not.toContain(optionToHide.displayName); + // Other options should still render + expect(optionTexts.length).toBe(colorFacet.options.length - 1); + }); + }); + + it('Should not render options with data.cio_plp_hidden = true', async () => { + const colorFacet = mockTransformedFacets.find((f) => f.name === 'color'); // lowercase + const facetWithHiddenOption = { + ...colorFacet, + options: colorFacet.options.map((option, index) => ({ + ...option, + data: { + ...option.data, + cio_plp_hidden: index === 0, // Hide first option + }, + })), + }; + + const { container } = render( + + + , + ); + + await waitFor(() => { + const colorFilterGroup = container.querySelector('.cio-filter-group'); + const optionLabels = colorFilterGroup.querySelectorAll('.cio-filter-multiple-option label'); + const optionTexts = Array.from(optionLabels).map((label) => label.textContent); + + // First option should not be rendered + expect(optionTexts).not.toContain(colorFacet.options[0].displayName); + // Other options should still render + expect(optionTexts.length).toBe(colorFacet.options.length - 1); + }); + }); + + it('Should hide facets from both isHiddenFacetFn and data.cio_plp_hidden', async () => { + const facetsWithHidden = mockTransformedFacets.map((facet, index) => ({ + ...facet, + data: { + ...facet.data, + cio_plp_hidden: index === 0, // Hide first facet via metadata + }, + })); + + const isHiddenFacetFn = (facet) => facet.name === facetsWithHidden[1].name; // Hide second facet via fn + + const { queryByText, container } = render( + + + , + ); + + await waitFor(() => { + // Both first and second facets should be hidden + expect(queryByText(facetsWithHidden[0].displayName)).not.toBeInTheDocument(); + expect(queryByText(facetsWithHidden[1].displayName)).not.toBeInTheDocument(); + // Remaining facets should render + const filterGroups = container.querySelectorAll('.cio-filter-group'); + expect(filterGroups.length).toBe(facetsWithHidden.length - 2); + }); + }); + }); }); diff --git a/spec/hooks/useFilter/useFilter.test.js b/spec/hooks/useFilter/useFilter.test.js index 31afab40..3cd4c99a 100644 --- a/spec/hooks/useFilter/useFilter.test.js +++ b/spec/hooks/useFilter/useFilter.test.js @@ -1,10 +1,13 @@ import '@testing-library/jest-dom'; import { renderHook, waitFor } from '@testing-library/react'; +import React from 'react'; import useFilter from '../../../src/hooks/useFilter'; import useRequestConfigs from '../../../src/hooks/useRequestConfigs'; import mockSearchResponse from '../../local_examples/apiSearchResponse.json'; import { transformSearchResponse } from '../../../src/utils'; import { renderHookWithCioPlp } from '../../test-utils'; +import { CioPlpProvider } from '../../../src/components/CioPlp'; +import { DEMO_API_KEY } from '../../../src/constants'; describe('Testing Hook: useFilter', () => { const originalWindowLocation = window.location; @@ -236,4 +239,154 @@ describe('Testing Hook: useFilter', () => { expect(requestConfig.section.toString()).toBe('Search Suggestions'); }); }); + + describe('isHiddenFacetFn', () => { + it('Should filter out facets when isHiddenFacetFn returns true', async () => { + const isHiddenFacetFn = (facet) => facet.name === 'brand'; // lowercase + const useFilterPropsWithHiddenFn = { + ...useFilterProps, + isHiddenFacetFn, + }; + const { result } = renderHookWithCioPlp(() => useFilter(useFilterPropsWithHiddenFn)); + + await waitFor(() => { + const { + current: { facets }, + } = result; + + expect(facets.length).toBe(searchData.response.facets.length - 1); + expect(facets.find((f) => f.name === 'brand')).toBeUndefined(); + }); + }); + + it('Should not filter facets when isHiddenFacetFn returns false', async () => { + const isHiddenFacetFn = () => false; + const useFilterPropsWithHiddenFn = { + ...useFilterProps, + isHiddenFacetFn, + }; + const { result } = renderHookWithCioPlp(() => useFilter(useFilterPropsWithHiddenFn)); + + await waitFor(() => { + const { + current: { facets }, + } = result; + + expect(facets.length).toBe(searchData.response.facets.length); + }); + }); + + it('Should filter facets by multiple conditions', async () => { + const isHiddenFacetFn = (facet) => facet.type === 'range'; + const useFilterPropsWithHiddenFn = { + ...useFilterProps, + isHiddenFacetFn, + }; + const { result } = renderHookWithCioPlp(() => useFilter(useFilterPropsWithHiddenFn)); + + await waitFor(() => { + const { + current: { facets }, + } = result; + + // Should filter out all range type facets + expect(facets.every((f) => f.type !== 'range')).toBe(true); + }); + }); + }); + + describe('getIsHiddenFacetField via metadata', () => { + it('Should filter out facets with data.cio_plp_hidden = true', async () => { + // Create facets with hidden flag + const facetsWithHidden = searchData.response.facets.map((facet, index) => ({ + ...facet, + data: { + ...facet.data, + cio_plp_hidden: index === 0, // Hide first facet + }, + })); + + const useFilterPropsWithHiddenData = { + facets: facetsWithHidden, + }; + const { result } = renderHookWithCioPlp(() => useFilter(useFilterPropsWithHiddenData)); + + await waitFor(() => { + const { + current: { facets }, + } = result; + + expect(facets.length).toBe(facetsWithHidden.length - 1); + // First facet should be hidden + expect(facets[0].name).not.toBe(facetsWithHidden[0].name); + }); + }); + + it('Should use custom getIsHiddenFacetField from itemFieldGetters', async () => { + // Create a custom field getter that hides facets with a custom field + const customGetIsHiddenFacetField = (facet) => facet.data?.customHidden === true; + + const facetsWithCustomField = searchData.response.facets.map((facet, index) => ({ + ...facet, + data: { + ...facet.data, + customHidden: index === 1, // Hide second facet + }, + })); + + const { result } = renderHook( + () => useFilter({ facets: facetsWithCustomField }), + { + wrapper: ({ children }) => ( + + {children} + + ), + } + ); + + await waitFor(() => { + const { + current: { facets }, + } = result; + + expect(facets.length).toBe(facetsWithCustomField.length - 1); + // Second facet should be hidden + expect(facets.find((f) => f.name === facetsWithCustomField[1].name)).toBeUndefined(); + }); + }); + + it('Should prioritize isHiddenFacetFn over getIsHiddenFacetField', async () => { + // Both methods should work together - if either returns true, facet is hidden + const facetsWithHidden = searchData.response.facets.map((facet, index) => ({ + ...facet, + data: { + ...facet.data, + cio_plp_hidden: index === 0, // Hide first facet via metadata + }, + })); + + const isHiddenFacetFn = (facet) => facet.name === facetsWithHidden[1].name; // Also hide second facet via fn + + const useFilterPropsWithBoth = { + facets: facetsWithHidden, + isHiddenFacetFn, + }; + const { result } = renderHookWithCioPlp(() => useFilter(useFilterPropsWithBoth)); + + await waitFor(() => { + const { + current: { facets }, + } = result; + + // Both first and second facets should be hidden + expect(facets.length).toBe(facetsWithHidden.length - 2); + expect(facets.find((f) => f.name === facetsWithHidden[0].name)).toBeUndefined(); + expect(facets.find((f) => f.name === facetsWithHidden[1].name)).toBeUndefined(); + }); + }); + }); }); diff --git a/src/components/Filters/FilterGroup.tsx b/src/components/Filters/FilterGroup.tsx index d955cbb9..e53f8397 100644 --- a/src/components/Filters/FilterGroup.tsx +++ b/src/components/Filters/FilterGroup.tsx @@ -1,5 +1,5 @@ import React, { useState } from 'react'; -import type { PlpFacet } from '../../types'; +import type { PlpFacet, PlpFacetOption } from '../../types'; import { isMultipleOrBucketedFacet, isRangeFacet } from '../../utils'; import FilterOptionsList from './FilterOptionsList'; import FilterRangeSlider from './FilterRangeSlider'; @@ -11,10 +11,22 @@ export interface FilterGroupProps { initialNumOptions?: number; sliderStep?: number; facetSliderSteps?: Record; + /** + * Function that takes in a PlpFacetOption and returns `true` if the option should be hidden from the final render + * @returns boolean + */ + isHiddenFacetOptionFn?: (option: PlpFacetOption) => boolean; } export default function FilterGroup(props: FilterGroupProps) { - const { facet, setFilter, initialNumOptions = 10, sliderStep, facetSliderSteps } = props; + const { + facet, + setFilter, + initialNumOptions = 10, + sliderStep, + facetSliderSteps, + isHiddenFacetOptionFn, + } = props; const [isCollapsed, setIsCollapsed] = useState(false); const toggleIsCollapsed = () => setIsCollapsed(!isCollapsed); @@ -35,6 +47,7 @@ export default function FilterGroup(props: FilterGroupProps) { multipleFacet={facet} modifyRequestMultipleFilter={onFilterSelect(facet.name)} initialNumOptions={initialNumOptions} + isHiddenFacetOptionFn={isHiddenFacetOptionFn} /> )} diff --git a/src/components/Filters/Filters.tsx b/src/components/Filters/Filters.tsx index 8c8869c0..79f1b1a0 100644 --- a/src/components/Filters/Filters.tsx +++ b/src/components/Filters/Filters.tsx @@ -1,6 +1,6 @@ /* eslint-disable react/no-array-index-key */ import React from 'react'; -import { IncludeRenderProps } from '../../types'; +import { IncludeRenderProps, PlpFacetOption } from '../../types'; import FilterGroup from './FilterGroup'; import useFilter, { UseFilterProps, UseFilterReturn } from '../../hooks/useFilter'; @@ -10,11 +10,16 @@ export type FiltersProps = UseFilterProps & { * The remaining options will be hidden under a "Show All" button */ initialNumOptions?: number; + /** + * Function that takes in a PlpFacetOption and returns `true` if the option should be hidden from the final render + * @returns boolean + */ + isHiddenFacetOptionFn?: (option: PlpFacetOption) => boolean; }; export type FiltersWithRenderProps = IncludeRenderProps; export default function Filters(props: FiltersWithRenderProps) { - const { children, initialNumOptions, ...useFiltersProps } = props; + const { children, initialNumOptions, isHiddenFacetOptionFn, ...useFiltersProps } = props; const { facets, setFilter, sliderStep, facetSliderSteps, clearFilters } = useFilter(useFiltersProps); @@ -37,6 +42,7 @@ export default function Filters(props: FiltersWithRenderProps) { setFilter={setFilter} sliderStep={sliderStep} facetSliderSteps={facetSliderSteps} + isHiddenFacetOptionFn={isHiddenFacetOptionFn} key={facet.name} /> ))} diff --git a/src/components/Filters/UseFilterOptionsList.tsx b/src/components/Filters/UseFilterOptionsList.tsx index c6c4d9f1..96d4a566 100644 --- a/src/components/Filters/UseFilterOptionsList.tsx +++ b/src/components/Filters/UseFilterOptionsList.tsx @@ -1,12 +1,18 @@ -import { useEffect, useState } from 'react'; -import { PlpMultipleFacet } from '../../types'; +import { useCallback, useEffect, useState } from 'react'; +import { PlpFacetOption, PlpMultipleFacet } from '../../types'; import useOptionsList from '../../hooks/useOptionsList'; +import { useCioPlpContext } from '../../hooks/useCioPlpContext'; export interface UseFilterOptionsListProps { multipleFacet: PlpMultipleFacet; modifyRequestMultipleFilter: (selectedOptions: Array | null) => void; initialNumOptions: number; isCollapsed: boolean; + /** + * Function that takes in a PlpFacetOption and returns `true` if the option should be hidden from the final render + * @returns boolean + */ + isHiddenFacetOptionFn?: (option: PlpFacetOption) => boolean; } export default function useFilterOptionsList(props: UseFilterOptionsListProps) { @@ -15,11 +21,23 @@ export default function useFilterOptionsList(props: UseFilterOptionsListProps) { initialNumOptions, modifyRequestMultipleFilter, isCollapsed, + isHiddenFacetOptionFn, } = props; + const { getIsHiddenFacetOptionField } = useCioPlpContext().itemFieldGetters; + + const isHiddenOptionFn = useCallback( + (option: PlpFacetOption) => + (typeof isHiddenFacetOptionFn === 'function' && isHiddenFacetOptionFn(option)) || + (typeof getIsHiddenFacetOptionField === 'function' && getIsHiddenFacetOptionField(option)) || + false, + [isHiddenFacetOptionFn, getIsHiddenFacetOptionField], + ); + const { isShowAll, setIsShowAll, optionsToRender, setOptionsToRender } = useOptionsList({ options: facet.options, initialNumOptions, + isHiddenOptionFn, }); const [selectedOptionMap, setSelectedOptionMap] = useState({}); diff --git a/src/hooks/useFilter.ts b/src/hooks/useFilter.ts index 9e8bf25b..e50bdb59 100644 --- a/src/hooks/useFilter.ts +++ b/src/hooks/useFilter.ts @@ -1,3 +1,4 @@ +import { useCallback, useMemo } from 'react'; import { useCioPlpContext } from './useCioPlpContext'; import { PlpFacet, PlpFilterValue } from '../types'; import useRequestConfigs from './useRequestConfigs'; @@ -23,18 +24,37 @@ export interface UseFilterProps { * Per-facet slider step configuration */ facetSliderSteps?: Record; + /** + * Function that takes in a PlpFacet and returns `true` if the facet should be hidden from the final render + * @returns boolean + */ + isHiddenFacetFn?: (facet: PlpFacet) => boolean; } export default function useFilter(props: UseFilterProps): UseFilterReturn { - const { facets, sliderStep, facetSliderSteps } = props; + const { facets, sliderStep, facetSliderSteps, isHiddenFacetFn } = props; const contextValue = useCioPlpContext(); if (!contextValue) { throw new Error('useFilter must be used within a component that is a child of '); } + const { getIsHiddenFacetField } = contextValue.itemFieldGetters; const { getRequestConfigs, setRequestConfigs } = useRequestConfigs(); + const isHiddenFacet = useCallback( + (facet: PlpFacet) => + (typeof isHiddenFacetFn === 'function' && isHiddenFacetFn(facet)) || + (typeof getIsHiddenFacetField === 'function' && getIsHiddenFacetField(facet)) || + false, + [isHiddenFacetFn, getIsHiddenFacetField], + ); + + const filteredFacets = useMemo( + () => facets.filter((facet) => !isHiddenFacet(facet)), + [facets, isHiddenFacet], + ); + const setFilter = (filterName: string, filterValue: PlpFilterValue) => { const newFilters = getRequestConfigs().filters || {}; @@ -52,7 +72,7 @@ export default function useFilter(props: UseFilterProps): UseFilterReturn { }; return { - facets, + facets: filteredFacets, setFilter, sliderStep, facetSliderSteps, diff --git a/src/stories/components/Filters/Code Examples.mdx b/src/stories/components/Filters/Code Examples.mdx index 51fba1b8..250b396a 100644 --- a/src/stories/components/Filters/Code Examples.mdx +++ b/src/stories/components/Filters/Code Examples.mdx @@ -83,3 +83,80 @@ export default function MyComponent() { `UseFilterReturn` {UseFilterReturn} + +## Hiding Facets and Options + +You can hide facets or individual facet options using two methods: + +### Method 1: Custom Function + +Use `isHiddenFacetFn` to hide entire facet groups and `isHiddenFacetOptionFn` to hide specific options. + +```jsx +import { CioPlp, CioPlpGrid, Filters } from '@constructor-io/constructorio-ui-plp'; +import '@constructor-io/constructorio-ui-plp/styles.css'; + +const DEMO_API_KEY = 'key_M57QS8SMPdLdLx4x'; + +export default function MyComponent() { + // Hide the "price" facet entirely + const isHiddenFacetFn = (facet) => facet.name === 'price'; + + // Hide specific color options + const isHiddenFacetOptionFn = (option) => + option.value === 'Black' || option.value === 'Blue'; + + return ( + + + {(props) => { + return ( + + ); + }} + + + ); +} +``` + + + + + +### Method 2: Metadata Field + +Add `cio_plp_hidden: true` to the `data` field of facets or facet options in your Constructor.io dashboard. +These items will be automatically hidden without requiring custom functions. + +```json +{ + "name": "internal_category", + "displayName": "Internal Category", + "type": "multiple", + "data": { + "cio_plp_hidden": true + } +} +``` + + + +### Custom Field Getter + +You can also customize the metadata field used for hiding by providing a custom `itemFieldGetters` to `CioPlpProvider`: + +```jsx + facet.data?.myCustomHiddenField, + getIsHiddenFacetOptionField: (option) => option.data?.myCustomHiddenField, + }}> + {/* ... */} + +``` diff --git a/src/stories/components/Filters/Filters.stories.tsx b/src/stories/components/Filters/Filters.stories.tsx index 62567a2b..e582c940 100644 --- a/src/stories/components/Filters/Filters.stories.tsx +++ b/src/stories/components/Filters/Filters.stories.tsx @@ -48,3 +48,46 @@ export const Primary: Story = { facets: mockTransformedFacets as Array, }, }; + +/** + * Use `isHiddenFacetFn` to hide entire facet groups based on custom logic. + * In this example, the "Color" facet is hidden. + */ +export const HiddenFacets: Story = { + render: (args) => , + args: { + facets: mockTransformedFacets as Array, + isHiddenFacetFn: (facet: PlpFacet) => facet.name === 'color', + }, +}; + +/** + * Use `isHiddenFacetOptionFn` to hide specific options within facets. + * In this example, the "Black" and "Blue" color options are hidden. + */ +export const HiddenFacetOptions: Story = { + render: (args) => , + args: { + facets: mockTransformedFacets as Array, + isHiddenFacetOptionFn: (option) => option.value === 'Black' || option.value === 'Blue', + initialNumOptions: 20, + }, +}; + +/** + * Facets and options with `data.cio_plp_hidden = true` in their metadata + * are automatically hidden without needing custom functions. + * This example shows facets with the hidden metadata flag. + */ +export const HiddenViaMetadata: Story = { + render: (args) => , + args: { + facets: (mockTransformedFacets as Array).map((facet, index) => ({ + ...facet, + data: { + ...facet.data, + cio_plp_hidden: index === 0, // Hide the first facet (Color) + }, + })), + }, +}; diff --git a/src/types.ts b/src/types.ts index fbc750ab..ff183b56 100644 --- a/src/types.ts +++ b/src/types.ts @@ -51,6 +51,8 @@ export interface ItemFieldGetters { retrieveRolloverImage: ItemFieldGetters['getRolloverImage'], ) => SwatchItem[] | undefined; getIsHiddenGroupField: (group: PlpItemGroup) => boolean | undefined; + getIsHiddenFacetField: (facet: PlpFacet) => boolean | undefined; + getIsHiddenFacetOptionField: (option: PlpFacetOption) => boolean | undefined; getItemUrl: (item: Item) => string | undefined; } diff --git a/src/utils/itemFieldGetters.ts b/src/utils/itemFieldGetters.ts index 737462eb..1b1b7099 100644 --- a/src/utils/itemFieldGetters.ts +++ b/src/utils/itemFieldGetters.ts @@ -1,4 +1,12 @@ -import { ItemFieldGetters, Item, SwatchItem, Variation, PlpItemGroup } from '../types'; +import { + ItemFieldGetters, + Item, + SwatchItem, + Variation, + PlpItemGroup, + PlpFacet, + PlpFacetOption, +} from '../types'; // eslint-disable-next-line import/prefer-default-export export function getPrice(item: Item | Variation): number { @@ -49,6 +57,14 @@ export function getIsHiddenGroupField(group: PlpItemGroup) { return group?.data?.cio_plp_hidden; } +export function getIsHiddenFacetField(facet: PlpFacet) { + return facet?.data?.cio_plp_hidden; +} + +export function getIsHiddenFacetOptionField(option: PlpFacetOption) { + return (option?.data as Record)?.cio_plp_hidden; +} + export function getItemUrl(item: Item): string | undefined { return item?.url; } From 41a3f2a9df96c7a1146002a6402657768b6db0e1 Mon Sep 17 00:00:00 2001 From: Enes Kutay SEZEN Date: Wed, 28 Jan 2026 13:48:14 +0300 Subject: [PATCH 2/7] Support hierarchical facets --- spec/hooks/useGroups/useGroups.test.js | 129 ++++++++++++++++++ .../Filters/UseFilterOptionsList.tsx | 1 + src/hooks/useGroups.ts | 1 + src/hooks/useOptionsList.ts | 44 +++++- 4 files changed, 170 insertions(+), 5 deletions(-) diff --git a/spec/hooks/useGroups/useGroups.test.js b/spec/hooks/useGroups/useGroups.test.js index 62e2a378..6758a86c 100644 --- a/spec/hooks/useGroups/useGroups.test.js +++ b/spec/hooks/useGroups/useGroups.test.js @@ -305,4 +305,133 @@ describe('Testing Hook: useGroups', () => { expect(excludedGroups.find((group) => group.groupId === 'cio_plp_hidden_group')).toBeTruthy(); }); }); + + describe('Recursive filtering of nested children', () => { + it('Should recursively filter out nested children matching isHiddenGroupFn', async () => { + // Create nested groups structure + const nestedGroups = [ + { + groupId: 'root', + displayName: 'Root', + count: 100, + data: null, + parents: [], + children: [ + { + groupId: 'child1', + displayName: 'Child 1', + count: 50, + data: null, + parents: [], + children: [ + { + groupId: 'grandchild1', + displayName: 'Grandchild 1', + count: 25, + data: null, + parents: [], + children: [], + }, + { + groupId: 'grandchild2-hidden', + displayName: 'Grandchild 2 Hidden', + count: 25, + data: null, + parents: [], + children: [], + }, + ], + }, + { + groupId: 'child2', + displayName: 'Child 2', + count: 50, + data: null, + parents: [], + children: [], + }, + ], + }, + ]; + + const isHiddenGroupFn = (group) => group.groupId.includes('hidden'); + + const { result } = renderHookWithCioPlp(() => + useGroups({ groups: nestedGroups, isHiddenGroupFn }), + ); + + await waitFor(() => { + const { + current: { optionsToRender }, + } = result; + + // Should have 2 top-level children (root is the container) + expect(optionsToRender.length).toBe(2); + + // Find child1 and check its nested children + const child1 = optionsToRender.find((g) => g.groupId === 'child1'); + expect(child1).toBeDefined(); + + // grandchild2-hidden should be filtered out recursively + expect(child1.children.length).toBe(1); + expect(child1.children[0].groupId).toBe('grandchild1'); + expect(child1.children.find((g) => g.groupId === 'grandchild2-hidden')).toBeUndefined(); + }); + }); + + it('Should recursively filter out nested children with data.cio_plp_hidden = true', async () => { + const nestedGroups = [ + { + groupId: 'root', + displayName: 'Root', + count: 100, + data: null, + parents: [], + children: [ + { + groupId: 'child1', + displayName: 'Child 1', + count: 50, + data: null, + parents: [], + children: [ + { + groupId: 'grandchild1', + displayName: 'Grandchild 1', + count: 25, + data: null, + parents: [], + children: [], + }, + { + groupId: 'grandchild2', + displayName: 'Grandchild 2', + count: 25, + data: { cio_plp_hidden: true }, + parents: [], + children: [], + }, + ], + }, + ], + }, + ]; + + const { result } = renderHookWithCioPlp(() => useGroups({ groups: nestedGroups })); + + await waitFor(() => { + const { + current: { optionsToRender }, + } = result; + + // Find child1 and check its nested children + const child1 = optionsToRender.find((g) => g.groupId === 'child1'); + expect(child1).toBeDefined(); + + // grandchild2 should be filtered out due to cio_plp_hidden flag + expect(child1.children.length).toBe(1); + expect(child1.children[0].groupId).toBe('grandchild1'); + }); + }); + }); }); diff --git a/src/components/Filters/UseFilterOptionsList.tsx b/src/components/Filters/UseFilterOptionsList.tsx index 96d4a566..031e26d2 100644 --- a/src/components/Filters/UseFilterOptionsList.tsx +++ b/src/components/Filters/UseFilterOptionsList.tsx @@ -38,6 +38,7 @@ export default function useFilterOptionsList(props: UseFilterOptionsListProps) { options: facet.options, initialNumOptions, isHiddenOptionFn, + nestedOptionsKey: 'options', // Enable recursive filtering for hierarchical facet options }); const [selectedOptionMap, setSelectedOptionMap] = useState({}); diff --git a/src/hooks/useGroups.ts b/src/hooks/useGroups.ts index 0a920e41..2a439a0d 100644 --- a/src/hooks/useGroups.ts +++ b/src/hooks/useGroups.ts @@ -59,6 +59,7 @@ export default function useGroups(props: UseGroupProps) { options: groupOptions, initialNumOptions: numOptionsProps, isHiddenOptionFn, + nestedOptionsKey: 'children', // Enable recursive filtering for group children }); const [selectedGroupId, setSelectedGroupId] = useState(); diff --git a/src/hooks/useOptionsList.ts b/src/hooks/useOptionsList.ts index 26af0240..bb3ebaaf 100644 --- a/src/hooks/useOptionsList.ts +++ b/src/hooks/useOptionsList.ts @@ -15,17 +15,51 @@ export interface UseOptionsListProps { * @returns boolean */ isHiddenOptionFn?: (option: T) => boolean; + /** + * Key name for nested options array (e.g., 'options' for hierarchical facets, 'children' for groups). + * When provided, filtering will be applied recursively to nested options. + */ + nestedOptionsKey?: string; } const defaultIsHiddenOptionFn = () => false; +/** + * Recursively filters options and their nested children + */ +function filterOptionsRecursively( + options: Array, + isHiddenFn: (option: T) => boolean, + nestedKey: string, +): Array { + return options + .filter((option) => !isHiddenFn(option)) + .map((option) => { + const nestedOptions = (option as Record)[nestedKey]; + if (Array.isArray(nestedOptions) && nestedOptions.length > 0) { + return { + ...option, + [nestedKey]: filterOptionsRecursively(nestedOptions, isHiddenFn, nestedKey), + }; + } + return option; + }); +} + export default function useOptionsList(props: UseOptionsListProps) { - const { options, initialNumOptions = 5, isHiddenOptionFn = defaultIsHiddenOptionFn } = props; + const { + options, + initialNumOptions = 5, + isHiddenOptionFn = defaultIsHiddenOptionFn, + nestedOptionsKey, + } = props; - const filteredOptions = useMemo( - () => options.filter((option) => !isHiddenOptionFn(option)), - [isHiddenOptionFn, options], - ); + const filteredOptions = useMemo(() => { + if (nestedOptionsKey) { + return filterOptionsRecursively(options, isHiddenOptionFn, nestedOptionsKey); + } + return options.filter((option) => !isHiddenOptionFn(option)); + }, [isHiddenOptionFn, options, nestedOptionsKey]); const [isShowAll, setIsShowAll] = useState(false); const [optionsToRender, setOptionsToRender] = useState>(filteredOptions); From 6c1ee939ca6e3568818b78ef07efbbaa6b9a2ab4 Mon Sep 17 00:00:00 2001 From: Enes Kutay SEZEN Date: Wed, 28 Jan 2026 14:11:55 +0300 Subject: [PATCH 3/7] Make types better --- src/types.ts | 4 ++-- src/utils/itemFieldGetters.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/types.ts b/src/types.ts index ff183b56..091985cf 100644 --- a/src/types.ts +++ b/src/types.ts @@ -372,14 +372,14 @@ export interface PlpFacetOption { count: number; displayName: string; value: string; - data: object; + data: Record; range?: ['-inf' | number, 'inf' | number]; options?: Array; } export interface PlpHierarchicalFacetOption extends PlpFacetOption { options: Array; - data: object & { parentValue: string | null }; + data: Record & { parentValue: string | null }; } export interface PlpItemGroup { diff --git a/src/utils/itemFieldGetters.ts b/src/utils/itemFieldGetters.ts index 1b1b7099..c0823d97 100644 --- a/src/utils/itemFieldGetters.ts +++ b/src/utils/itemFieldGetters.ts @@ -62,7 +62,7 @@ export function getIsHiddenFacetField(facet: PlpFacet) { } export function getIsHiddenFacetOptionField(option: PlpFacetOption) { - return (option?.data as Record)?.cio_plp_hidden; + return option?.data?.cio_plp_hidden; } export function getItemUrl(item: Item): string | undefined { From 238ba0c70505d5c4819061915d7bfd5727f8fe24 Mon Sep 17 00:00:00 2001 From: Enes Kutay SEZEN Date: Wed, 28 Jan 2026 14:28:34 +0300 Subject: [PATCH 4/7] Update examples --- .../components/Filters/Code Examples.mdx | 47 +++++++++++++++---- .../components/Filters/Filters.stories.tsx | 8 ++-- 2 files changed, 42 insertions(+), 13 deletions(-) diff --git a/src/stories/components/Filters/Code Examples.mdx b/src/stories/components/Filters/Code Examples.mdx index 250b396a..50dbeb8d 100644 --- a/src/stories/components/Filters/Code Examples.mdx +++ b/src/stories/components/Filters/Code Examples.mdx @@ -90,7 +90,9 @@ You can hide facets or individual facet options using two methods: ### Method 1: Custom Function -Use `isHiddenFacetFn` to hide entire facet groups and `isHiddenFacetOptionFn` to hide specific options. +#### Hiding Entire Facets + +Use `isHiddenFacetFn` to hide entire facet groups. In this example, the "Price" facet is hidden: ```jsx import { CioPlp, CioPlpGrid, Filters } from '@constructor-io/constructorio-ui-plp'; @@ -102,6 +104,36 @@ export default function MyComponent() { // Hide the "price" facet entirely const isHiddenFacetFn = (facet) => facet.name === 'price'; + return ( + + + {(props) => { + return ( + + ); + }} + + + ); +} +``` + + + +#### Hiding Specific Options + +Use `isHiddenFacetOptionFn` to hide specific options within facets. In this example, the "Black" and "Blue" color options are hidden: + +```jsx +import { CioPlp, CioPlpGrid, Filters } from '@constructor-io/constructorio-ui-plp'; +import '@constructor-io/constructorio-ui-plp/styles.css'; + +const DEMO_API_KEY = 'key_M57QS8SMPdLdLx4x'; + +export default function MyComponent() { // Hide specific color options const isHiddenFacetOptionFn = (option) => option.value === 'Black' || option.value === 'Blue'; @@ -113,7 +145,6 @@ export default function MyComponent() { return ( ); @@ -124,8 +155,6 @@ export default function MyComponent() { } ``` - - ### Method 2: Metadata Field @@ -135,9 +164,9 @@ These items will be automatically hidden without requiring custom functions. ```json { - "name": "internal_category", - "displayName": "Internal Category", - "type": "multiple", + "name": "price", + "displayName": "Price", + "type": "range", "data": { "cio_plp_hidden": true } @@ -154,8 +183,8 @@ You can also customize the metadata field used for hiding by providing a custom facet.data?.myCustomHiddenField, - getIsHiddenFacetOptionField: (option) => option.data?.myCustomHiddenField, + getIsHiddenFacetField: (facet) => facet.data?.myCustomField, + getIsHiddenFacetOptionField: (option) => option.data?.myCustomField, }}> {/* ... */} diff --git a/src/stories/components/Filters/Filters.stories.tsx b/src/stories/components/Filters/Filters.stories.tsx index e582c940..92738e9c 100644 --- a/src/stories/components/Filters/Filters.stories.tsx +++ b/src/stories/components/Filters/Filters.stories.tsx @@ -51,13 +51,13 @@ export const Primary: Story = { /** * Use `isHiddenFacetFn` to hide entire facet groups based on custom logic. - * In this example, the "Color" facet is hidden. + * In this example, the "Price" facet is hidden. */ export const HiddenFacets: Story = { render: (args) => , args: { facets: mockTransformedFacets as Array, - isHiddenFacetFn: (facet: PlpFacet) => facet.name === 'color', + isHiddenFacetFn: (facet: PlpFacet) => facet.name === 'price', }, }; @@ -82,11 +82,11 @@ export const HiddenFacetOptions: Story = { export const HiddenViaMetadata: Story = { render: (args) => , args: { - facets: (mockTransformedFacets as Array).map((facet, index) => ({ + facets: (mockTransformedFacets as Array).map((facet) => ({ ...facet, data: { ...facet.data, - cio_plp_hidden: index === 0, // Hide the first facet (Color) + cio_plp_hidden: facet.name === 'price', // Hide the Price facet }, })), }, From fd66dded5563b22b9f2fd298f3a095d0b72f5d23 Mon Sep 17 00:00:00 2001 From: Enes Kutay SEZEN Date: Wed, 28 Jan 2026 14:33:32 +0300 Subject: [PATCH 5/7] rename --- spec/components/Filters/Filters.test.jsx | 18 +++++------ spec/hooks/useFilter/useFilter.test.js | 32 +++++++++---------- src/components/Filters/FilterGroup.tsx | 6 ++-- src/components/Filters/Filters.tsx | 6 ++-- .../Filters/UseFilterOptionsList.tsx | 12 +++---- src/hooks/useFilter.ts | 18 +++++------ .../components/Filters/Code Examples.mdx | 20 ++++++------ .../components/Filters/Filters.stories.tsx | 12 +++---- src/types.ts | 4 +-- src/utils/itemFieldGetters.ts | 4 +-- 10 files changed, 66 insertions(+), 66 deletions(-) diff --git a/spec/components/Filters/Filters.test.jsx b/spec/components/Filters/Filters.test.jsx index 1fbd27d2..72252137 100644 --- a/spec/components/Filters/Filters.test.jsx +++ b/spec/components/Filters/Filters.test.jsx @@ -755,11 +755,11 @@ describe('Testing Component: Filters', () => { }); describe(' - Facet Blacklisting Tests', () => { - it('Should not render facets when isHiddenFacetFn returns true', async () => { - const isHiddenFacetFn = (facet) => facet.name === 'color'; // lowercase 'color' + it('Should not render facets when isHiddenFilterFn returns true', async () => { + const isHiddenFilterFn = (facet) => facet.name === 'color'; // lowercase 'color' const { queryByText, getByText } = render( - + , ); @@ -795,14 +795,14 @@ describe('Testing Component: Filters', () => { }); }); - it('Should not render options when isHiddenFacetOptionFn returns true', async () => { + it('Should not render options when isHiddenFilterOptionFn returns true', async () => { const colorFacet = mockTransformedFacets.find((f) => f.name === 'color'); // lowercase const optionToHide = colorFacet.options[0]; - const isHiddenFacetOptionFn = (option) => option.value === optionToHide.value; + const isHiddenFilterOptionFn = (option) => option.value === optionToHide.value; const { container } = render( - + , ); @@ -849,7 +849,7 @@ describe('Testing Component: Filters', () => { }); }); - it('Should hide facets from both isHiddenFacetFn and data.cio_plp_hidden', async () => { + it('Should hide facets from both isHiddenFilterFn and data.cio_plp_hidden', async () => { const facetsWithHidden = mockTransformedFacets.map((facet, index) => ({ ...facet, data: { @@ -858,11 +858,11 @@ describe('Testing Component: Filters', () => { }, })); - const isHiddenFacetFn = (facet) => facet.name === facetsWithHidden[1].name; // Hide second facet via fn + const isHiddenFilterFn = (facet) => facet.name === facetsWithHidden[1].name; // Hide second facet via fn const { queryByText, container } = render( - + , ); diff --git a/spec/hooks/useFilter/useFilter.test.js b/spec/hooks/useFilter/useFilter.test.js index 3cd4c99a..0aa04b22 100644 --- a/spec/hooks/useFilter/useFilter.test.js +++ b/spec/hooks/useFilter/useFilter.test.js @@ -240,12 +240,12 @@ describe('Testing Hook: useFilter', () => { }); }); - describe('isHiddenFacetFn', () => { - it('Should filter out facets when isHiddenFacetFn returns true', async () => { - const isHiddenFacetFn = (facet) => facet.name === 'brand'; // lowercase + describe('isHiddenFilterFn', () => { + it('Should filter out facets when isHiddenFilterFn returns true', async () => { + const isHiddenFilterFn = (facet) => facet.name === 'brand'; // lowercase const useFilterPropsWithHiddenFn = { ...useFilterProps, - isHiddenFacetFn, + isHiddenFilterFn, }; const { result } = renderHookWithCioPlp(() => useFilter(useFilterPropsWithHiddenFn)); @@ -259,11 +259,11 @@ describe('Testing Hook: useFilter', () => { }); }); - it('Should not filter facets when isHiddenFacetFn returns false', async () => { - const isHiddenFacetFn = () => false; + it('Should not filter facets when isHiddenFilterFn returns false', async () => { + const isHiddenFilterFn = () => false; const useFilterPropsWithHiddenFn = { ...useFilterProps, - isHiddenFacetFn, + isHiddenFilterFn, }; const { result } = renderHookWithCioPlp(() => useFilter(useFilterPropsWithHiddenFn)); @@ -277,10 +277,10 @@ describe('Testing Hook: useFilter', () => { }); it('Should filter facets by multiple conditions', async () => { - const isHiddenFacetFn = (facet) => facet.type === 'range'; + const isHiddenFilterFn = (facet) => facet.type === 'range'; const useFilterPropsWithHiddenFn = { ...useFilterProps, - isHiddenFacetFn, + isHiddenFilterFn, }; const { result } = renderHookWithCioPlp(() => useFilter(useFilterPropsWithHiddenFn)); @@ -295,7 +295,7 @@ describe('Testing Hook: useFilter', () => { }); }); - describe('getIsHiddenFacetField via metadata', () => { + describe('getIsHiddenFilterField via metadata', () => { it('Should filter out facets with data.cio_plp_hidden = true', async () => { // Create facets with hidden flag const facetsWithHidden = searchData.response.facets.map((facet, index) => ({ @@ -322,9 +322,9 @@ describe('Testing Hook: useFilter', () => { }); }); - it('Should use custom getIsHiddenFacetField from itemFieldGetters', async () => { + it('Should use custom getIsHiddenFilterField from itemFieldGetters', async () => { // Create a custom field getter that hides facets with a custom field - const customGetIsHiddenFacetField = (facet) => facet.data?.customHidden === true; + const customGetIsHiddenFilterField = (facet) => facet.data?.customHidden === true; const facetsWithCustomField = searchData.response.facets.map((facet, index) => ({ ...facet, @@ -340,7 +340,7 @@ describe('Testing Hook: useFilter', () => { wrapper: ({ children }) => ( {children} @@ -359,7 +359,7 @@ describe('Testing Hook: useFilter', () => { }); }); - it('Should prioritize isHiddenFacetFn over getIsHiddenFacetField', async () => { + it('Should prioritize isHiddenFilterFn over getIsHiddenFilterField', async () => { // Both methods should work together - if either returns true, facet is hidden const facetsWithHidden = searchData.response.facets.map((facet, index) => ({ ...facet, @@ -369,11 +369,11 @@ describe('Testing Hook: useFilter', () => { }, })); - const isHiddenFacetFn = (facet) => facet.name === facetsWithHidden[1].name; // Also hide second facet via fn + const isHiddenFilterFn = (facet) => facet.name === facetsWithHidden[1].name; // Also hide second facet via fn const useFilterPropsWithBoth = { facets: facetsWithHidden, - isHiddenFacetFn, + isHiddenFilterFn, }; const { result } = renderHookWithCioPlp(() => useFilter(useFilterPropsWithBoth)); diff --git a/src/components/Filters/FilterGroup.tsx b/src/components/Filters/FilterGroup.tsx index e53f8397..bdfde873 100644 --- a/src/components/Filters/FilterGroup.tsx +++ b/src/components/Filters/FilterGroup.tsx @@ -15,7 +15,7 @@ export interface FilterGroupProps { * Function that takes in a PlpFacetOption and returns `true` if the option should be hidden from the final render * @returns boolean */ - isHiddenFacetOptionFn?: (option: PlpFacetOption) => boolean; + isHiddenFilterOptionFn?: (option: PlpFacetOption) => boolean; } export default function FilterGroup(props: FilterGroupProps) { @@ -25,7 +25,7 @@ export default function FilterGroup(props: FilterGroupProps) { initialNumOptions = 10, sliderStep, facetSliderSteps, - isHiddenFacetOptionFn, + isHiddenFilterOptionFn, } = props; const [isCollapsed, setIsCollapsed] = useState(false); @@ -47,7 +47,7 @@ export default function FilterGroup(props: FilterGroupProps) { multipleFacet={facet} modifyRequestMultipleFilter={onFilterSelect(facet.name)} initialNumOptions={initialNumOptions} - isHiddenFacetOptionFn={isHiddenFacetOptionFn} + isHiddenFilterOptionFn={isHiddenFilterOptionFn} /> )} diff --git a/src/components/Filters/Filters.tsx b/src/components/Filters/Filters.tsx index 79f1b1a0..3b125eab 100644 --- a/src/components/Filters/Filters.tsx +++ b/src/components/Filters/Filters.tsx @@ -14,12 +14,12 @@ export type FiltersProps = UseFilterProps & { * Function that takes in a PlpFacetOption and returns `true` if the option should be hidden from the final render * @returns boolean */ - isHiddenFacetOptionFn?: (option: PlpFacetOption) => boolean; + isHiddenFilterOptionFn?: (option: PlpFacetOption) => boolean; }; export type FiltersWithRenderProps = IncludeRenderProps; export default function Filters(props: FiltersWithRenderProps) { - const { children, initialNumOptions, isHiddenFacetOptionFn, ...useFiltersProps } = props; + const { children, initialNumOptions, isHiddenFilterOptionFn, ...useFiltersProps } = props; const { facets, setFilter, sliderStep, facetSliderSteps, clearFilters } = useFilter(useFiltersProps); @@ -42,7 +42,7 @@ export default function Filters(props: FiltersWithRenderProps) { setFilter={setFilter} sliderStep={sliderStep} facetSliderSteps={facetSliderSteps} - isHiddenFacetOptionFn={isHiddenFacetOptionFn} + isHiddenFilterOptionFn={isHiddenFilterOptionFn} key={facet.name} /> ))} diff --git a/src/components/Filters/UseFilterOptionsList.tsx b/src/components/Filters/UseFilterOptionsList.tsx index 031e26d2..b9454c10 100644 --- a/src/components/Filters/UseFilterOptionsList.tsx +++ b/src/components/Filters/UseFilterOptionsList.tsx @@ -12,7 +12,7 @@ export interface UseFilterOptionsListProps { * Function that takes in a PlpFacetOption and returns `true` if the option should be hidden from the final render * @returns boolean */ - isHiddenFacetOptionFn?: (option: PlpFacetOption) => boolean; + isHiddenFilterOptionFn?: (option: PlpFacetOption) => boolean; } export default function useFilterOptionsList(props: UseFilterOptionsListProps) { @@ -21,17 +21,17 @@ export default function useFilterOptionsList(props: UseFilterOptionsListProps) { initialNumOptions, modifyRequestMultipleFilter, isCollapsed, - isHiddenFacetOptionFn, + isHiddenFilterOptionFn, } = props; - const { getIsHiddenFacetOptionField } = useCioPlpContext().itemFieldGetters; + const { getIsHiddenFilterOptionField } = useCioPlpContext().itemFieldGetters; const isHiddenOptionFn = useCallback( (option: PlpFacetOption) => - (typeof isHiddenFacetOptionFn === 'function' && isHiddenFacetOptionFn(option)) || - (typeof getIsHiddenFacetOptionField === 'function' && getIsHiddenFacetOptionField(option)) || + (typeof isHiddenFilterOptionFn === 'function' && isHiddenFilterOptionFn(option)) || + (typeof getIsHiddenFilterOptionField === 'function' && getIsHiddenFilterOptionField(option)) || false, - [isHiddenFacetOptionFn, getIsHiddenFacetOptionField], + [isHiddenFilterOptionFn, getIsHiddenFilterOptionField], ); const { isShowAll, setIsShowAll, optionsToRender, setOptionsToRender } = useOptionsList({ diff --git a/src/hooks/useFilter.ts b/src/hooks/useFilter.ts index e50bdb59..7a05a1f5 100644 --- a/src/hooks/useFilter.ts +++ b/src/hooks/useFilter.ts @@ -28,31 +28,31 @@ export interface UseFilterProps { * Function that takes in a PlpFacet and returns `true` if the facet should be hidden from the final render * @returns boolean */ - isHiddenFacetFn?: (facet: PlpFacet) => boolean; + isHiddenFilterFn?: (facet: PlpFacet) => boolean; } export default function useFilter(props: UseFilterProps): UseFilterReturn { - const { facets, sliderStep, facetSliderSteps, isHiddenFacetFn } = props; + const { facets, sliderStep, facetSliderSteps, isHiddenFilterFn } = props; const contextValue = useCioPlpContext(); if (!contextValue) { throw new Error('useFilter must be used within a component that is a child of '); } - const { getIsHiddenFacetField } = contextValue.itemFieldGetters; + const { getIsHiddenFilterField } = contextValue.itemFieldGetters; const { getRequestConfigs, setRequestConfigs } = useRequestConfigs(); - const isHiddenFacet = useCallback( + const isHiddenFilter = useCallback( (facet: PlpFacet) => - (typeof isHiddenFacetFn === 'function' && isHiddenFacetFn(facet)) || - (typeof getIsHiddenFacetField === 'function' && getIsHiddenFacetField(facet)) || + (typeof isHiddenFilterFn === 'function' && isHiddenFilterFn(facet)) || + (typeof getIsHiddenFilterField === 'function' && getIsHiddenFilterField(facet)) || false, - [isHiddenFacetFn, getIsHiddenFacetField], + [isHiddenFilterFn, getIsHiddenFilterField], ); const filteredFacets = useMemo( - () => facets.filter((facet) => !isHiddenFacet(facet)), - [facets, isHiddenFacet], + () => facets.filter((facet) => !isHiddenFilter(facet)), + [facets, isHiddenFilter], ); const setFilter = (filterName: string, filterValue: PlpFilterValue) => { diff --git a/src/stories/components/Filters/Code Examples.mdx b/src/stories/components/Filters/Code Examples.mdx index 50dbeb8d..a0d3dc14 100644 --- a/src/stories/components/Filters/Code Examples.mdx +++ b/src/stories/components/Filters/Code Examples.mdx @@ -92,7 +92,7 @@ You can hide facets or individual facet options using two methods: #### Hiding Entire Facets -Use `isHiddenFacetFn` to hide entire facet groups. In this example, the "Price" facet is hidden: +Use `isHiddenFilterFn` to hide entire facet groups. In this example, the "Price" facet is hidden: ```jsx import { CioPlp, CioPlpGrid, Filters } from '@constructor-io/constructorio-ui-plp'; @@ -102,7 +102,7 @@ const DEMO_API_KEY = 'key_M57QS8SMPdLdLx4x'; export default function MyComponent() { // Hide the "price" facet entirely - const isHiddenFacetFn = (facet) => facet.name === 'price'; + const isHiddenFilterFn = (facet) => facet.name === 'price'; return ( @@ -111,7 +111,7 @@ export default function MyComponent() { return ( ); }} @@ -121,11 +121,11 @@ export default function MyComponent() { } ``` - + #### Hiding Specific Options -Use `isHiddenFacetOptionFn` to hide specific options within facets. In this example, the "Black" and "Blue" color options are hidden: +Use `isHiddenFilterOptionFn` to hide specific options within facets. In this example, the "Black" and "Blue" color options are hidden: ```jsx import { CioPlp, CioPlpGrid, Filters } from '@constructor-io/constructorio-ui-plp'; @@ -135,7 +135,7 @@ const DEMO_API_KEY = 'key_M57QS8SMPdLdLx4x'; export default function MyComponent() { // Hide specific color options - const isHiddenFacetOptionFn = (option) => + const isHiddenFilterOptionFn = (option) => option.value === 'Black' || option.value === 'Blue'; return ( @@ -145,7 +145,7 @@ export default function MyComponent() { return ( ); }} @@ -155,7 +155,7 @@ export default function MyComponent() { } ``` - + ### Method 2: Metadata Field @@ -183,8 +183,8 @@ You can also customize the metadata field used for hiding by providing a custom facet.data?.myCustomField, - getIsHiddenFacetOptionField: (option) => option.data?.myCustomField, + getIsHiddenFilterField: (facet) => facet.data?.myCustomField, + getIsHiddenFilterOptionField: (option) => option.data?.myCustomField, }}> {/* ... */} diff --git a/src/stories/components/Filters/Filters.stories.tsx b/src/stories/components/Filters/Filters.stories.tsx index 92738e9c..2314d76e 100644 --- a/src/stories/components/Filters/Filters.stories.tsx +++ b/src/stories/components/Filters/Filters.stories.tsx @@ -50,26 +50,26 @@ export const Primary: Story = { }; /** - * Use `isHiddenFacetFn` to hide entire facet groups based on custom logic. + * Use `isHiddenFilterFn` to hide entire facet groups based on custom logic. * In this example, the "Price" facet is hidden. */ -export const HiddenFacets: Story = { +export const HiddenFilters: Story = { render: (args) => , args: { facets: mockTransformedFacets as Array, - isHiddenFacetFn: (facet: PlpFacet) => facet.name === 'price', + isHiddenFilterFn: (facet: PlpFacet) => facet.name === 'price', }, }; /** - * Use `isHiddenFacetOptionFn` to hide specific options within facets. + * Use `isHiddenFilterOptionFn` to hide specific options within facets. * In this example, the "Black" and "Blue" color options are hidden. */ -export const HiddenFacetOptions: Story = { +export const HiddenFilterOptions: Story = { render: (args) => , args: { facets: mockTransformedFacets as Array, - isHiddenFacetOptionFn: (option) => option.value === 'Black' || option.value === 'Blue', + isHiddenFilterOptionFn: (option) => option.value === 'Black' || option.value === 'Blue', initialNumOptions: 20, }, }; diff --git a/src/types.ts b/src/types.ts index 091985cf..c048d1b5 100644 --- a/src/types.ts +++ b/src/types.ts @@ -51,8 +51,8 @@ export interface ItemFieldGetters { retrieveRolloverImage: ItemFieldGetters['getRolloverImage'], ) => SwatchItem[] | undefined; getIsHiddenGroupField: (group: PlpItemGroup) => boolean | undefined; - getIsHiddenFacetField: (facet: PlpFacet) => boolean | undefined; - getIsHiddenFacetOptionField: (option: PlpFacetOption) => boolean | undefined; + getIsHiddenFilterField: (facet: PlpFacet) => boolean | undefined; + getIsHiddenFilterOptionField: (option: PlpFacetOption) => boolean | undefined; getItemUrl: (item: Item) => string | undefined; } diff --git a/src/utils/itemFieldGetters.ts b/src/utils/itemFieldGetters.ts index c0823d97..3e0a2c69 100644 --- a/src/utils/itemFieldGetters.ts +++ b/src/utils/itemFieldGetters.ts @@ -57,11 +57,11 @@ export function getIsHiddenGroupField(group: PlpItemGroup) { return group?.data?.cio_plp_hidden; } -export function getIsHiddenFacetField(facet: PlpFacet) { +export function getIsHiddenFilterField(facet: PlpFacet) { return facet?.data?.cio_plp_hidden; } -export function getIsHiddenFacetOptionField(option: PlpFacetOption) { +export function getIsHiddenFilterOptionField(option: PlpFacetOption) { return option?.data?.cio_plp_hidden; } From 85cf4a0f7b0b3794a8f4705e9cff32e52263a507 Mon Sep 17 00:00:00 2001 From: Enes Kutay SEZEN Date: Wed, 28 Jan 2026 14:44:50 +0300 Subject: [PATCH 6/7] Update docs --- src/stories/components/Filters/Code Examples.mdx | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/stories/components/Filters/Code Examples.mdx b/src/stories/components/Filters/Code Examples.mdx index a0d3dc14..e987ef24 100644 --- a/src/stories/components/Filters/Code Examples.mdx +++ b/src/stories/components/Filters/Code Examples.mdx @@ -86,7 +86,9 @@ export default function MyComponent() { ## Hiding Facets and Options -You can hide facets or individual facet options using two methods: +You can hide facets or individual facet options using three methods: + +> **Note:** This functionality is intended for **conditional rendering per page**. For example, showing a facet on search results but hiding it on category pages. If a facet or option should be hidden across your entire site, use the hidden facets feature in the Constructor.io dashboard or API instead. Hidden facets configured through the dashboard are excluded from API responses entirely, which is more efficient than client-side filtering. ### Method 1: Custom Function @@ -175,7 +177,7 @@ These items will be automatically hidden without requiring custom functions. -### Custom Field Getter +### Method 3: Custom Field Getter You can also customize the metadata field used for hiding by providing a custom `itemFieldGetters` to `CioPlpProvider`: From 62f3bcea46507d576190dcc4a38211a8e882e76a Mon Sep 17 00:00:00 2001 From: Enes Kutay SEZEN Date: Wed, 28 Jan 2026 14:45:34 +0300 Subject: [PATCH 7/7] Lint --- src/components/Filters/UseFilterOptionsList.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/components/Filters/UseFilterOptionsList.tsx b/src/components/Filters/UseFilterOptionsList.tsx index b9454c10..fe20c3f7 100644 --- a/src/components/Filters/UseFilterOptionsList.tsx +++ b/src/components/Filters/UseFilterOptionsList.tsx @@ -29,7 +29,8 @@ export default function useFilterOptionsList(props: UseFilterOptionsListProps) { const isHiddenOptionFn = useCallback( (option: PlpFacetOption) => (typeof isHiddenFilterOptionFn === 'function' && isHiddenFilterOptionFn(option)) || - (typeof getIsHiddenFilterOptionField === 'function' && getIsHiddenFilterOptionField(option)) || + (typeof getIsHiddenFilterOptionField === 'function' && + getIsHiddenFilterOptionField(option)) || false, [isHiddenFilterOptionFn, getIsHiddenFilterOptionField], );