Skip to content

Commit fbeaef8

Browse files
author
Viktor Zavala
committedMar 18, 2025·
Merge branch 'main' into ci-4083-autocomplete-os-ui-enable-forcing-into-testing-cells-for-ui

File tree

10 files changed

+218
-154
lines changed

10 files changed

+218
-154
lines changed
 

‎package-lock.json

+129-149
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎spec/Components/CioAutocomplete/CioAutocomplete.test.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -221,7 +221,7 @@ describe('CioAutocomplete Client-Side Rendering', () => {
221221

222222
options.forEach((option) => {
223223
const suggestionCount = option.querySelector('.cio-suggestion-count');
224-
expect(suggestionCount).toBeInTheDocument();
224+
expect(suggestionCount).not.toBeInTheDocument();
225225
});
226226
});
227227
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import React, { ReactNode, useContext, useMemo, useEffect, useRef, isValidElement } from 'react';
2+
import { Item } from '../../../types';
3+
import { CioAutocompleteContext } from '../CioAutocompleteProvider';
4+
5+
interface CustomItemProps {
6+
item: Item;
7+
renderItem: (props: {
8+
item: Item;
9+
query: string;
10+
getItemProps: (item: Item) => any;
11+
}) => HTMLElement | ReactNode;
12+
query: string;
13+
}
14+
15+
export default function CustomSectionItem(props: CustomItemProps) {
16+
const { renderItem, item, query } = props;
17+
const { getItemProps } = useContext(CioAutocompleteContext);
18+
const ref = useRef<HTMLDivElement>(null);
19+
20+
// eslint-disable-next-line react-hooks/exhaustive-deps
21+
const customElement = useMemo(() => renderItem({ item, query, getItemProps }), [item]);
22+
23+
const isDomElement = customElement instanceof HTMLElement;
24+
const isReactNode = isValidElement(customElement);
25+
26+
useEffect(() => {
27+
if (isDomElement && ref.current) {
28+
ref.current.innerHTML = ''; // Clear previous content
29+
ref.current.appendChild(customElement);
30+
}
31+
// eslint-disable-next-line react-hooks/exhaustive-deps
32+
}, [item]);
33+
34+
if (isReactNode) {
35+
return customElement;
36+
}
37+
38+
if (isDomElement) {
39+
return <div {...getItemProps(item)} ref={ref} />;
40+
}
41+
return null;
42+
}

‎src/components/Autocomplete/SectionItemsList/SectionItemsList.tsx

+10-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import React, { ReactElement, useContext } from 'react';
22
import { Section } from '../../../types';
33
import SectionItem from '../SectionItem/SectionItem';
4+
import CustomSectionItem from '../SectionItem/CustomSectionItem';
45
import { translate } from '../../../utils/helpers';
56
import { camelToStartCase } from '../../../utils/format';
67
import { CioAutocompleteContext } from '../CioAutocompleteProvider';
@@ -18,7 +19,7 @@ type SectionItemsListProps = {
1819

1920
// eslint-disable-next-line func-names
2021
const DefaultRenderSectionItemsList: RenderSectionItemsList = function ({ section }) {
21-
const { getSectionProps, query, getFormProps, advancedParameters, getItemProps } =
22+
const { getSectionProps, query, getFormProps, advancedParameters } =
2223
useContext(CioAutocompleteContext);
2324
const { displayShowAllResultsButton, translations } = advancedParameters || {};
2425
const { onSubmit } = getFormProps();
@@ -64,7 +65,14 @@ const DefaultRenderSectionItemsList: RenderSectionItemsList = function ({ sectio
6465
<ul className='cio-section-items' role='none'>
6566
{section?.data?.map((item) => {
6667
if (typeof section?.renderItem === 'function') {
67-
return section.renderItem({ item, query, getItemProps });
68+
return (
69+
<CustomSectionItem
70+
renderItem={section.renderItem}
71+
item={item}
72+
query={query}
73+
key={item.id}
74+
/>
75+
);
6876
}
6977
return (
7078
<SectionItem

‎src/constants.ts

+1
Original file line numberDiff line numberDiff line change
@@ -262,5 +262,6 @@ export const translationsDescription = `Pass a \`translations\` object to displa
262262

263263
export const customRenderItemDescription = `Customize the rendering of individual items within a Section by providing a \`renderItem\` function. This function allows you to define how each item should be rendered.
264264
- Make sure to call \`getItemProps(item)\` and spread like this \`<div {...getItemProps(item)}/>\` on the container of each custom rendered item or else tracking will not work.
265+
- Supports both ReactNode and HTMLElement as a return type
265266
`;
266267
export const displayShowAllResultsButtonDescription = `Pass a boolean to \`displayShowAllResultsButton\` to display a button at the bottom of the Products section to show all results. This button will submit the form and trigger the \`onSubmit\` callback.`;

‎src/hooks/useCioAutocomplete.ts

+3
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import useSections from './useSections';
2121
import useRecommendationsObserver from './useRecommendationsObserver';
2222
import { isCustomSection, isRecommendationsSection } from '../typeGuards';
2323
import useNormalizedProps from './useNormalizedProps';
24+
import useCustomBlur from './useCustomBlur';
2425

2526
export const defaultSections: UserDefinedSection[] = [
2627
{
@@ -88,6 +89,8 @@ const useCioAutocomplete = (options: UseCioAutocompleteOptions) => {
8889
...rest,
8990
});
9091

92+
useCustomBlur(isOpen, closeMenu, autocompleteClassName);
93+
9194
// Log console errors
9295
useConsoleErrors(sections, activeSections);
9396

‎src/hooks/useCustomBlur.ts

+21
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import { useCallback, useEffect } from 'react';
2+
3+
const useCustomBlur = (isOpen: boolean, closeMenu: () => void, autocompleteClassName: string) => {
4+
const handleDocumentClick = useCallback(
5+
(event: MouseEvent) => {
6+
if (isOpen && !(event.target as HTMLElement)?.closest(`.${autocompleteClassName}`)) {
7+
closeMenu();
8+
}
9+
},
10+
[closeMenu, isOpen, autocompleteClassName]
11+
);
12+
13+
useEffect(() => {
14+
document.addEventListener('mousedown', handleDocumentClick);
15+
return () => {
16+
document.removeEventListener('mousedown', handleDocumentClick);
17+
};
18+
}, [handleDocumentClick]);
19+
};
20+
21+
export default useCustomBlur;

‎src/hooks/useDownShift.ts

+9
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,15 @@ const useDownShift: UseDownShift = ({
6969
}
7070
}
7171
},
72+
stateReducer: (state, actionAndChanges) => {
73+
const { type, changes } = actionAndChanges;
74+
75+
// Override dropdown close on blur
76+
if (type === useCombobox.stateChangeTypes.InputBlur) {
77+
return { ...changes, isOpen: state.isOpen };
78+
}
79+
return changes;
80+
},
7281
...rest,
7382
});
7483

‎src/stories/tests/ComponentTests.stories.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -484,7 +484,7 @@ InGroupSuggestions.play = async ({ canvasElement }) => {
484484
const canvas = within(canvasElement);
485485
await userEvent.type(canvas.getByTestId('cio-input'), 'socks', { delay: 100 });
486486
await sleep(1000);
487-
expect(canvas.getAllByText('in Socks').length).toEqual(1);
487+
expect(canvas.getAllByText('in Socks & Underwear').length).toEqual(1);
488488
};
489489

490490
export const InGroupSuggestionsTwo = ComponentTemplate.bind({});

‎src/stories/tests/HooksTests.stories.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -414,7 +414,7 @@ InGroupSuggestions.play = async ({ canvasElement }) => {
414414
const canvas = within(canvasElement);
415415
await userEvent.type(canvas.getByTestId('cio-input'), 'socks', { delay: 100 });
416416
await sleep(1000);
417-
expect(canvas.getAllByText('in Socks').length).toEqual(1);
417+
expect(canvas.getAllByText('in Socks & Underwear').length).toEqual(1);
418418
};
419419

420420
export const InGroupSuggestionsTwo = HooksTemplate.bind({});

0 commit comments

Comments
 (0)
Please sign in to comment.