Skip to content
2 changes: 1 addition & 1 deletion .storybook/custom-styles-story.css
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
right: 24px;
}

.cio-autocomplete.custom-autocomplete-styles .cio-sectionName {
.cio-autocomplete.custom-autocomplete-styles .cio-section-name {
margin: 5px 3px;
}

Expand Down
16 changes: 8 additions & 8 deletions .storybook/full-example-styles-story.css
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@
right: 0;
}

.cio-autocomplete.full-example-autocomplete-styles .cio-sectionName {
.cio-autocomplete.full-example-autocomplete-styles .cio-section-name {
margin: 5px 3px;
}

Expand All @@ -72,25 +72,25 @@
color: rgb(70, 70, 70);
}

.cio-autocomplete.full-example-autocomplete-styles .products.cio-section {
.cio-autocomplete.full-example-autocomplete-styles .cio-section-products {
padding: 0px;
}

.cio-autocomplete.full-example-autocomplete-styles .products .cio-item-Products {
.cio-autocomplete.full-example-autocomplete-styles .cio-section-products .cio-item {
width: 120px !important;
height: fit-content;
padding: 15px;
}

.cio-autocomplete.full-example-autocomplete-styles .products .cio-section-items {
.cio-autocomplete.full-example-autocomplete-styles .cio-section-products .cio-section-items {
text-align: center;
}

.cio-autocomplete.full-example-autocomplete-styles .products .cio-product-text {
.cio-autocomplete.full-example-autocomplete-styles .cio-section-products .cio-product-text {
padding: 15px 5px 0;
}

.cio-autocomplete.full-example-autocomplete-styles .products .cio-item .cio-product-image {
.cio-autocomplete.full-example-autocomplete-styles .cio-section-products .cio-item .cio-product-image {
width: 100%;
height: 100%;
min-height: 200px;
Expand All @@ -104,7 +104,7 @@
border-radius: 0px;
}

.cio-autocomplete.full-example-autocomplete-styles .products p {
.cio-autocomplete.full-example-autocomplete-styles .cio-section-products p {
padding: 5px 5px 0;
}

Expand All @@ -125,7 +125,7 @@
width: unset;
}

.cio-autocomplete.full-example-autocomplete-styles .Products.cio-section {
.cio-autocomplete.full-example-autocomplete-styles .cio-section-products {
display: none;
}
}
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ function YourComponent() {
{sections?.map((section) => (
<div key={section.indexSectionName} className={section.indexSectionName}>
<div className='cio-section'>
<div className='cio-sectionName'>
<div className='cio-section-name'>
{section?.displayName || section.indexSectionName}
</div>
<div className='cio-items'>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,9 +39,10 @@ const DefaultRenderSectionItemsList: RenderSectionItemsList = function ({ sectio

if (!section?.data?.length) return null;

// @deprecated `cio-sectionName` will be removed in the next major release
return (
<li {...getSectionProps(section)}>
<h5 className='cio-sectionName' aria-hidden>
<h5 className='cio-section-name cio-sectionName' aria-hidden>
{camelToStartCase(sectionTitle)}
</h5>
<ul className='cio-section-items' role='none'>
Expand Down
2 changes: 1 addition & 1 deletion src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -188,7 +188,7 @@ import '@constructor-io/constructorio-ui-autocomplete/styles.css';
right: 24px;
}

.cio-autocomplete.custom-autocomplete-styles .cio-sectionName {
.cio-autocomplete.custom-autocomplete-styles .cio-section-name {
margin: 5px 3px;
}

Expand Down
61 changes: 39 additions & 22 deletions src/hooks/useCioAutocomplete.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import {
import useConsoleErrors from './useConsoleErrors';
import useSections from './useSections';
import useRecommendationsObserver from './useRecommendationsObserver';
import { isAutocompleteSection, isRecommendationsSection } from '../typeGuards';
import { isAutocompleteSection, isCustomSection, isRecommendationsSection } from '../typeGuards';

export const defaultSections: UserDefinedSection[] = [
{
Expand Down Expand Up @@ -144,6 +144,7 @@ const useCioAutocomplete = (options: UseCioAutocompleteOptions) => {

return {
...getItemProps({ item, index }),
// @deprecated `sectionItemTestId` will be removed as a className in the next major version
className: `cio-item ${sectionItemTestId}`,
'data-testid': sectionItemTestId,
};
Expand Down Expand Up @@ -212,37 +213,53 @@ const useCioAutocomplete = (options: UseCioAutocompleteOptions) => {
'data-testid': 'cio-form',
}),
getSectionProps: (section: Section) => {
const { type } = section;
let sectionTitle: string;
// @deprecated ClassNames derived from this fn will be removed in the next major version
const getDeprecatedClassNames = () => {
const { type } = section;
let sectionTitle: string;

// Add the indexSectionName as a class to the section container to make sure it gets the styles
const indexSectionName =
type !== 'custom' && section.indexSectionName
? toKebabCase(section.indexSectionName)
: '';

switch (type) {
case 'recommendations':
sectionTitle = section.podId;
break;
case 'autocomplete':
sectionTitle = section.displayName || section.indexSectionName;
break;
case 'custom':
sectionTitle = section.displayName;
break;
default:
sectionTitle = section.displayName || section.indexSectionName;
break;
}

return `${sectionTitle} ${indexSectionName}`;
};

// Always add the indexSectionName (defaults to Products) as a class to the section container for the styles
// Even if the section is a recommendation pod, if the results are "Products" or "Search Suggestions"
// ... they should be styled accordingly
const indexSectionName =
type !== 'custom' && section.indexSectionName ? toKebabCase(section.indexSectionName) : '';

switch (type) {
case 'recommendations':
sectionTitle = section.podId;
break;
case 'autocomplete':
sectionTitle = section.displayName || section.indexSectionName;
break;
case 'custom':
sectionTitle = section.displayName;
break;
default:
sectionTitle = section.displayName || section.indexSectionName;
break;
}
const sectionListingType = isCustomSection(section)
? 'custom'
: toKebabCase(section.indexSectionName || section.data[0]?.section || 'Products');

const attributes: HTMLPropsWithCioDataAttributes = {
className: `${sectionTitle} cio-section ${indexSectionName}`,
className: `cio-section cio-section-${sectionListingType} ${getDeprecatedClassNames()}`,
ref: section.ref,
role: 'none',
'data-cnstrc-section': section.data[0]?.section,
};

if (isCustomSection(section)) {
attributes['data-cnstrc-custom-section'] = true;
attributes['data-cnstrc-custom-section-name'] = section.displayName;
}

// Add data attributes for recommendations
if (isRecommendationsSection(section)) {
attributes['data-cnstrc-recommendations'] = true;
Expand Down
59 changes: 37 additions & 22 deletions src/stories/Autocomplete/Hook/index.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
/* eslint-disable no-param-reassign */
import React from 'react';
import useCioAutocomplete from '../../../hooks/useCioAutocomplete';
import { isRecommendationsSection } from '../../../typeGuards';
import { isCustomSection, isRecommendationsSection } from '../../../typeGuards';
import { Item } from '../../../types';
import { getStoryParams, toKebabCase } from '../../../utils';
import { camelToStartCase, getStoryParams, toKebabCase } from '../../../utils';

export function HooksTemplate(args) {
const {
Expand Down Expand Up @@ -119,32 +119,41 @@ export function HooksTemplate(args) {
if (!section?.data?.length) {
return null;
}

const { type, displayName } = section;
let sectionName = section.displayName;
const sectionListingType = isCustomSection(section)
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

Choose a reason for hiding this comment

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

We could. The reason why its included here is because we've rendered a separate div with the relevant classes in Line 154 so we kind of have to duplicate the logic to get the required values here.

Took a look at our history and it might be something we've included prior to creating getSectionProps so we might be good to omit this entirely from the story. @esezen would love to get your thoughts here since you touched it previously.

Copy link
Contributor

Choose a reason for hiding this comment

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

In terms of passing this variable via getSectionProps itself, I thought getSectionProps is meant to contain only the attributes we're going to render into the HTML? I would be against adding something in specifically for our Storybook.

? 'custom'
: toKebabCase(section.indexSectionName || section.data[0]?.section || 'Products');

let sectionTitle: string;
Copy link
Contributor

Choose a reason for hiding this comment

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

Same for sectionTitle. Is there a way we can get this from getSectionProps instead?

switch (type) {
case 'recommendations':
sectionName = section.podId;
sectionTitle = section.podId;
break;
case 'custom':
sectionName = toKebabCase(displayName);
sectionTitle = displayName;
break;
case 'autocomplete':
sectionName = section.indexSectionName;
sectionTitle = section.indexSectionName;
break;
default:
sectionName = section.indexSectionName;
sectionTitle = section.indexSectionName;
break;
Comment on lines 139 to 141
Copy link
Contributor

Choose a reason for hiding this comment

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

This is (type) broken currently but we still need it. Sections can be one of type autocomplete | recommendations | custom, but the type on autocomplete is optional meaning it can be undefined.

There's two ways to fix this:

  1. Define an additional type that mimics AutocompleteConfiguration but with no type
  2. Empty the default clause here. Add an if-conditional above to check if type===undefined
  3. Assign the type during convertLegacyParametersAndAddDefaults in useCioAutocomplete (preferred)

I'll pull up a separate PR for this.

Copy link
Contributor

Choose a reason for hiding this comment

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

which part you mean "This is (type) broken currently"? @Mudaafi

Copy link
Contributor

Choose a reason for hiding this comment

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

@mocca102 Might be my configuration, but since we've explicitly defined the type Section to be one of three things and the switch statement accounts for all three, the final default type is never. TS then throws the error Property 'indexSectionName' does not exist on type 'never'

}

const recommendationsSection = isRecommendationsSection(section)
? section.indexSectionName
: '';
if (displayName) {
sectionTitle = displayName;
}

let sectionClassNames = toKebabCase(sectionListingType);
if (isRecommendationsSection(section)) {
sectionClassNames += ` ${toKebabCase(section.podId)}`;
}

return (
<div key={sectionName} className={`${sectionName} ${recommendationsSection}`}>
<div key={sectionTitle} className={sectionClassNames}>
<div {...getSectionProps(section)}>
<h5 className='cio-sectionName'>{section.displayName || sectionName}</h5>
<h5 className='cio-section-name'>{camelToStartCase(sectionTitle)}</h5>
<div className='cio-section-items'>
{section?.data?.map((item) => renderItem(item))}
</div>
Expand Down Expand Up @@ -271,32 +280,38 @@ function YourComponent() {
if (!section?.data?.length) {
return null;
}

const { type, displayName } = section;
let sectionName = section.displayName;
let sectionTitle: string;

switch (type) {
case 'recommendations':
sectionName = section.podId;
sectionTitle = section.podId;
break;
case 'custom':
sectionName = toKebabCase(displayName);
sectionTitle = displayName;
break;
case 'autocomplete':
sectionName = section.indexSectionName;
sectionTitle = section.indexSectionName;
break;
default:
sectionName = section.indexSectionName;
sectionTitle = section.indexSectionName;
break;
}

const recommendationsSection = isRecommendationsSection(section)
? section.indexSectionName
: '';
if (displayName) {
sectionTitle = displayName;
}

let sectionClassNames = toKebabCase(sectionTitle);
if (isRecommendationsSection(section)) {
sectionClassNames += \` \${toKebabCase(section.indexSectionName)}\`;
}

return (
<div key={sectionName} className={\`\${sectionName} \${recommendationsSection}\`}>
<div key={sectionTitle} className={sectionClassNames}>
<div {...getSectionProps(section)}>
<h5 className='cio-sectionName'>{sectionName}</h5>
<h5 className='cio-section-name'>{camelToStartCase(sectionTitle)}</h5>
<div className='cio-section-items'>
{section?.data?.map((item) => renderItem(item))}
</div>
Expand Down
26 changes: 26 additions & 0 deletions src/stories/tests/ComponentTests.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -223,6 +223,19 @@ TypeSearchTermRenderSectionsDefaultOrder.play = async ({ canvasElement }) => {
expect(canvas.getAllByTestId('cio-item-Products').length).toBeGreaterThan(0);
expect(canvas.getAllByText('Best Sellers').length).toBeGreaterThan(0);

expect(canvas.getByTestId('cio-results').children[0].className).toContain('cio-section');
expect(canvas.getByTestId('cio-results').children[0].className).toContain(
'cio-section-search-suggestions'
);

expect(canvas.getByTestId('cio-results').children[1].className).toContain('cio-section');
expect(canvas.getByTestId('cio-results').children[1].className).toContain('cio-section-products');

// bestsellers indexSectionName is products, and we render class based on that
expect(canvas.getByTestId('cio-results').children[2].className).toContain('cio-section');
expect(canvas.getByTestId('cio-results').children[2].className).toContain('cio-section-products');

// @deprecated The following classNames will be removed in the next major version
expect(canvas.getByTestId('cio-results').children[0].className).toContain('Search Suggestions');
expect(canvas.getByTestId('cio-results').children[1].className).toContain('Products');
expect(canvas.getByTestId('cio-results').children[2].className).toContain('bestsellers');
Expand Down Expand Up @@ -254,6 +267,19 @@ TypeSearchTermRenderSectionsCustomOrder.play = async ({ canvasElement }) => {
expect(canvas.getAllByTestId('cio-item-Products').length).toBeGreaterThan(0);
expect(canvas.getAllByText('Best Sellers').length).toBeGreaterThan(0);

expect(canvas.getByTestId('cio-results').children[0].className).toContain('cio-section');
expect(canvas.getByTestId('cio-results').children[0].className).toContain('cio-section-products');

// bestsellers indexSectionName is products, and we render class based on that
expect(canvas.getByTestId('cio-results').children[1].className).toContain('cio-section');
expect(canvas.getByTestId('cio-results').children[1].className).toContain('cio-section-products');

expect(canvas.getByTestId('cio-results').children[2].className).toContain('cio-section');
expect(canvas.getByTestId('cio-results').children[2].className).toContain(
'cio-section-search-suggestions'
);

// @deprecated The following classNames will be removed in the next major version
expect(canvas.getByTestId('cio-results').children[0].className).toContain('Products');
expect(canvas.getByTestId('cio-results').children[1].className).toContain('bestsellers');
expect(canvas.getByTestId('cio-results').children[2].className).toContain('Search Suggestions');
Expand Down
11 changes: 7 additions & 4 deletions src/stories/tests/HooksTests.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -217,9 +217,10 @@ TypeSearchTermRenderSectionsDefaultOrder.play = async ({ canvasElement }) => {
expect(canvas.getAllByTestId('cio-item-Products').length).toBeGreaterThan(0);
expect(canvas.getAllByText('Best Sellers').length).toBeGreaterThan(0);

expect(canvas.getByTestId('cio-results').children[0].className).toContain('Search Suggestions');
expect(canvas.getByTestId('cio-results').children[1].className).toContain('Products');
expect(canvas.getByTestId('cio-results').children[0].className).toContain('search-suggestions');
expect(canvas.getByTestId('cio-results').children[1].className).toContain('products');
expect(canvas.getByTestId('cio-results').children[2].className).toContain('bestsellers');
expect(canvas.getByTestId('cio-results').children[2].className).toContain('products');
};

// - type search term => render all sections in custom order
Expand All @@ -239,6 +240,7 @@ TypeSearchTermRenderSectionsCustomOrder.args = {
},
],
};

TypeSearchTermRenderSectionsCustomOrder.play = async ({ canvasElement }) => {
const canvas = within(canvasElement);
await userEvent.type(canvas.getByTestId('cio-input'), 'red', { delay: 100 });
Expand All @@ -248,9 +250,10 @@ TypeSearchTermRenderSectionsCustomOrder.play = async ({ canvasElement }) => {
expect(canvas.getAllByTestId('cio-item-Products').length).toBeGreaterThan(0);
expect(canvas.getAllByText('Best Sellers').length).toBeGreaterThan(0);

expect(canvas.getByTestId('cio-results').children[0].className).toContain('Products');
expect(canvas.getByTestId('cio-results').children[0].className).toContain('products');
expect(canvas.getByTestId('cio-results').children[1].className).toContain('bestsellers');
expect(canvas.getByTestId('cio-results').children[2].className).toContain('Search Suggestions');
expect(canvas.getByTestId('cio-results').children[1].className).toContain('products');
expect(canvas.getByTestId('cio-results').children[2].className).toContain('search-suggestions');
};

// - select term suggestion => network tracking event
Expand Down
Loading