diff --git a/spec/components/Filters/Filters.test.jsx b/spec/components/Filters/Filters.test.jsx index f81e8241..72252137 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 isHiddenFilterFn returns true', async () => { + const isHiddenFilterFn = (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 isHiddenFilterOptionFn returns true', async () => { + const colorFacet = mockTransformedFacets.find((f) => f.name === 'color'); // lowercase + const optionToHide = colorFacet.options[0]; + const isHiddenFilterOptionFn = (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 isHiddenFilterFn 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 isHiddenFilterFn = (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..0aa04b22 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('isHiddenFilterFn', () => { + it('Should filter out facets when isHiddenFilterFn returns true', async () => { + const isHiddenFilterFn = (facet) => facet.name === 'brand'; // lowercase + const useFilterPropsWithHiddenFn = { + ...useFilterProps, + isHiddenFilterFn, + }; + 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 isHiddenFilterFn returns false', async () => { + const isHiddenFilterFn = () => false; + const useFilterPropsWithHiddenFn = { + ...useFilterProps, + isHiddenFilterFn, + }; + 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 isHiddenFilterFn = (facet) => facet.type === 'range'; + const useFilterPropsWithHiddenFn = { + ...useFilterProps, + isHiddenFilterFn, + }; + 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('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) => ({ + ...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 getIsHiddenFilterField from itemFieldGetters', async () => { + // Create a custom field getter that hides facets with a custom field + const customGetIsHiddenFilterField = (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 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, + data: { + ...facet.data, + cio_plp_hidden: index === 0, // Hide first facet via metadata + }, + })); + + const isHiddenFilterFn = (facet) => facet.name === facetsWithHidden[1].name; // Also hide second facet via fn + + const useFilterPropsWithBoth = { + facets: facetsWithHidden, + isHiddenFilterFn, + }; + 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/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/FilterGroup.tsx b/src/components/Filters/FilterGroup.tsx index d955cbb9..bdfde873 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 + */ + isHiddenFilterOptionFn?: (option: PlpFacetOption) => boolean; } export default function FilterGroup(props: FilterGroupProps) { - const { facet, setFilter, initialNumOptions = 10, sliderStep, facetSliderSteps } = props; + const { + facet, + setFilter, + initialNumOptions = 10, + sliderStep, + facetSliderSteps, + isHiddenFilterOptionFn, + } = 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} + isHiddenFilterOptionFn={isHiddenFilterOptionFn} /> )} diff --git a/src/components/Filters/Filters.tsx b/src/components/Filters/Filters.tsx index 8c8869c0..3b125eab 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 + */ + isHiddenFilterOptionFn?: (option: PlpFacetOption) => boolean; }; export type FiltersWithRenderProps = IncludeRenderProps; export default function Filters(props: FiltersWithRenderProps) { - const { children, initialNumOptions, ...useFiltersProps } = props; + const { children, initialNumOptions, isHiddenFilterOptionFn, ...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} + isHiddenFilterOptionFn={isHiddenFilterOptionFn} key={facet.name} /> ))} diff --git a/src/components/Filters/UseFilterOptionsList.tsx b/src/components/Filters/UseFilterOptionsList.tsx index c6c4d9f1..fe20c3f7 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 + */ + isHiddenFilterOptionFn?: (option: PlpFacetOption) => boolean; } export default function useFilterOptionsList(props: UseFilterOptionsListProps) { @@ -15,11 +21,25 @@ export default function useFilterOptionsList(props: UseFilterOptionsListProps) { initialNumOptions, modifyRequestMultipleFilter, isCollapsed, + isHiddenFilterOptionFn, } = props; + const { getIsHiddenFilterOptionField } = useCioPlpContext().itemFieldGetters; + + const isHiddenOptionFn = useCallback( + (option: PlpFacetOption) => + (typeof isHiddenFilterOptionFn === 'function' && isHiddenFilterOptionFn(option)) || + (typeof getIsHiddenFilterOptionField === 'function' && + getIsHiddenFilterOptionField(option)) || + false, + [isHiddenFilterOptionFn, getIsHiddenFilterOptionField], + ); + const { isShowAll, setIsShowAll, optionsToRender, setOptionsToRender } = useOptionsList({ options: facet.options, initialNumOptions, + isHiddenOptionFn, + nestedOptionsKey: 'options', // Enable recursive filtering for hierarchical facet options }); const [selectedOptionMap, setSelectedOptionMap] = useState({}); diff --git a/src/hooks/useFilter.ts b/src/hooks/useFilter.ts index 9e8bf25b..7a05a1f5 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 + */ + isHiddenFilterFn?: (facet: PlpFacet) => boolean; } export default function useFilter(props: UseFilterProps): UseFilterReturn { - const { facets, sliderStep, facetSliderSteps } = 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 { getIsHiddenFilterField } = contextValue.itemFieldGetters; const { getRequestConfigs, setRequestConfigs } = useRequestConfigs(); + const isHiddenFilter = useCallback( + (facet: PlpFacet) => + (typeof isHiddenFilterFn === 'function' && isHiddenFilterFn(facet)) || + (typeof getIsHiddenFilterField === 'function' && getIsHiddenFilterField(facet)) || + false, + [isHiddenFilterFn, getIsHiddenFilterField], + ); + + const filteredFacets = useMemo( + () => facets.filter((facet) => !isHiddenFilter(facet)), + [facets, isHiddenFilter], + ); + 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/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); diff --git a/src/stories/components/Filters/Code Examples.mdx b/src/stories/components/Filters/Code Examples.mdx index 51fba1b8..e987ef24 100644 --- a/src/stories/components/Filters/Code Examples.mdx +++ b/src/stories/components/Filters/Code Examples.mdx @@ -83,3 +83,111 @@ export default function MyComponent() { `UseFilterReturn` {UseFilterReturn} + +## Hiding Facets and Options + +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 + +#### Hiding Entire Facets + +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'; +import '@constructor-io/constructorio-ui-plp/styles.css'; + +const DEMO_API_KEY = 'key_M57QS8SMPdLdLx4x'; + +export default function MyComponent() { + // Hide the "price" facet entirely + const isHiddenFilterFn = (facet) => facet.name === 'price'; + + return ( + + + {(props) => { + return ( + + ); + }} + + + ); +} +``` + + + +#### Hiding Specific Options + +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'; +import '@constructor-io/constructorio-ui-plp/styles.css'; + +const DEMO_API_KEY = 'key_M57QS8SMPdLdLx4x'; + +export default function MyComponent() { + // Hide specific color options + const isHiddenFilterOptionFn = (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": "price", + "displayName": "Price", + "type": "range", + "data": { + "cio_plp_hidden": true + } +} +``` + + + +### Method 3: Custom Field Getter + +You can also customize the metadata field used for hiding by providing a custom `itemFieldGetters` to `CioPlpProvider`: + +```jsx + 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 62567a2b..2314d76e 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 `isHiddenFilterFn` to hide entire facet groups based on custom logic. + * In this example, the "Price" facet is hidden. + */ +export const HiddenFilters: Story = { + render: (args) => , + args: { + facets: mockTransformedFacets as Array, + isHiddenFilterFn: (facet: PlpFacet) => facet.name === 'price', + }, +}; + +/** + * Use `isHiddenFilterOptionFn` to hide specific options within facets. + * In this example, the "Black" and "Blue" color options are hidden. + */ +export const HiddenFilterOptions: Story = { + render: (args) => , + args: { + facets: mockTransformedFacets as Array, + isHiddenFilterOptionFn: (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) => ({ + ...facet, + data: { + ...facet.data, + cio_plp_hidden: facet.name === 'price', // Hide the Price facet + }, + })), + }, +}; diff --git a/src/types.ts b/src/types.ts index fbc750ab..c048d1b5 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; + getIsHiddenFilterField: (facet: PlpFacet) => boolean | undefined; + getIsHiddenFilterOptionField: (option: PlpFacetOption) => boolean | undefined; getItemUrl: (item: Item) => string | undefined; } @@ -370,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 737462eb..3e0a2c69 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 getIsHiddenFilterField(facet: PlpFacet) { + return facet?.data?.cio_plp_hidden; +} + +export function getIsHiddenFilterOptionField(option: PlpFacetOption) { + return option?.data?.cio_plp_hidden; +} + export function getItemUrl(item: Item): string | undefined { return item?.url; }