Skip to content

Commit

Permalink
feat(infinite-hits): add top banner to InfiniteHits widget (#6243)
Browse files Browse the repository at this point in the history
* wip(infinite-hits): add banner to js component and widget

* feat(infinite-hits): use DefaultBanner component

* test(infinite-hits): update snapshots with banner elements

* chore(infinite-hits): update css templates to style infinite hits banner

* feat(infinite-hits): add banner to React version

* test(infinite-hits): update css class list

* fix(infinite-hits): correctly pass data to banner template

* test(hits): update common widget tests
  • Loading branch information
taylorcjohnson authored Jun 27, 2024
1 parent 762e633 commit 9287532
Show file tree
Hide file tree
Showing 14 changed files with 894 additions and 63 deletions.
6 changes: 4 additions & 2 deletions packages/instantsearch.css/src/themes/algolia.scss
Original file line number Diff line number Diff line change
Expand Up @@ -334,12 +334,14 @@ a[class^='ais-'] {
margin-bottom: 1rem;
}

.ais-Hits-banner {
.ais-Hits-banner,
.ais-InfiniteHits-banner {
display: flex;
justify-content: center;
}

.ais-Hits-banner-image {
.ais-Hits-banner-image
.ais-InfiniteHits-banner-image {
max-width: 100%;
}

Expand Down
6 changes: 4 additions & 2 deletions packages/instantsearch.css/src/themes/satellite.scss
Original file line number Diff line number Diff line change
Expand Up @@ -658,12 +658,14 @@ $break-medium: 767px;
margin: 1rem auto;
}

.ais-Hits-banner {
.ais-Hits-banner,
.ais-InfiniteHits-banner {
display: flex;
justify-content: center;
}

.ais-Hits-banner-image {
.ais-Hits-banner-image,
.ais-InfiniteHits-banner-image {
max-width: 100%;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,11 @@ import type {
InfiniteHitsCSSClasses,
InfiniteHitsTemplates,
} from '../../widgets/infinite-hits/infinite-hits';
import type { SearchResults } from 'algoliasearch-helper';
import type { Banner, SearchResults } from 'algoliasearch-helper';

export type InfiniteHitsComponentCSSClasses =
ComponentCSSClasses<InfiniteHitsCSSClasses>;
export type InfiniteHitsComponentTemplates = Required<InfiniteHitsTemplates>;
export type InfiniteHitsComponentTemplates = InfiniteHitsTemplates;

export type InfiniteHitsProps = {
cssClasses: InfiniteHitsComponentCSSClasses;
Expand All @@ -35,6 +35,46 @@ export type InfiniteHitsProps = {
insights?: InsightsClient;
sendEvent: SendEventForHits;
bindEvent: BindEventForHits;
banner?: Banner;
};

const DefaultBanner = ({
banner,
classNames,
}: {
banner: Banner;
classNames: Pick<
InfiniteHitsCSSClasses,
'bannerRoot' | 'bannerLink' | 'bannerImage'
>;
}) => {
if (!banner.image.urls[0].url) {
return null;
}

return (
<aside className={cx(classNames.bannerRoot)}>
{banner.link ? (
<a
className={cx(classNames.bannerLink)}
href={banner.link.url}
target={banner.link.target}
>
<img
className={cx(classNames.bannerImage)}
src={banner.image.urls[0].url}
alt={banner.image.title}
/>
</a>
) : (
<img
className={cx(classNames.bannerImage)}
src={banner.image.urls[0].url}
alt={banner.image.title}
/>
)}
</aside>
);
};

const InfiniteHits = ({
Expand All @@ -50,6 +90,7 @@ const InfiniteHits = ({
isLastPage,
cssClasses,
templateProps,
banner,
}: InfiniteHitsProps) => {
const handleInsightsClick = createInsightsEventHandler({
insights,
Expand All @@ -58,15 +99,31 @@ const InfiniteHits = ({

if (results.hits.length === 0) {
return (
<Template
{...templateProps}
templateKey="empty"
rootProps={{
className: cx(cssClasses.root, cssClasses.emptyRoot),
onClick: handleInsightsClick,
}}
data={results}
/>
<div
className={cx(cssClasses.root, cssClasses.emptyRoot)}
onClick={handleInsightsClick}
>
{banner &&
(templateProps.templates.banner ? (
<Template
{...templateProps}
templateKey="banner"
rootTagName="fragment"
data={{
banner,
className: cssClasses.bannerRoot,
}}
/>
) : (
<DefaultBanner banner={banner} classNames={cssClasses} />
))}
<Template
{...templateProps}
templateKey="empty"
rootTagName="fragment"
data={results}
/>
</div>
);
}

Expand All @@ -88,6 +145,21 @@ const InfiniteHits = ({
/>
)}

{banner &&
(templateProps.templates.banner ? (
<Template
{...templateProps}
templateKey="banner"
rootTagName="fragment"
data={{
banner,
className: cssClasses.bannerRoot,
}}
/>
) : (
<DefaultBanner banner={banner} classNames={cssClasses} />
))}

<ol className={cssClasses.list}>
{hits.map((hit, index) => (
<Template
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,9 @@ describe('InfiniteHits', () => {
disabledLoadPrevious: 'disabledLoadPrevious',
loadMore: 'loadMore',
disabledLoadMore: 'disabledLoadMore',
bannerRoot: 'bannerRoot',
bannerImage: 'bannerImage',
bannerLink: 'bannerLink',
};

const sendEvent = () => {};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,9 @@ describe('InfiniteHits', () => {
disabledLoadPrevious: '',
loadMore: '',
disabledLoadMore: '',
bannerRoot: '',
bannerImage: '',
bannerLink: '',
},
templateProps: prepareTemplateProps({
defaultTemplates: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,21 @@ import infiniteHits from '../infinite-hits';
import type { SearchResponse } from '../../../../src/types';
import type { MockSearchClient } from '@instantsearch/mocks';

const bannerWidgetRenderingContent = {
widgets: {
banners: [
{
image: {
urls: [{ url: 'https://via.placeholder.com/550x250' }],
},
link: {
url: 'https://www.algolia.com',
},
},
],
},
};

beforeEach(() => {
document.body.innerHTML = '';
});
Expand Down Expand Up @@ -50,7 +65,11 @@ describe('infiniteHits', () => {

test('adds custom CSS classes', async () => {
const container = document.createElement('div');
const searchClient = createMockedSearchClient();
const searchClient = createMockedSearchClient({
// @TODO: remove once algoliasearch js client has been updated
// @ts-expect-error
renderingContent: bannerWidgetRenderingContent,
});

const search = instantsearch({
indexName: 'indexName',
Expand Down Expand Up @@ -78,6 +97,9 @@ describe('infiniteHits', () => {
loadMore: 'LOAD_MORE',
disabledLoadPrevious: 'DISABLED_LOAD_PREVIOUS',
disabledLoadMore: 'DISABLED_LOAD_MORE',
bannerRoot: 'BANNER_ROOT',
bannerImage: 'BANNER_IMAGE',
bannerLink: 'BANNER_LINK',
},
}),
]);
Expand All @@ -99,6 +121,15 @@ describe('infiniteHits', () => {
expect(container.querySelector('.ais-InfiniteHits-loadMore')).toHaveClass(
'LOAD_MORE DISABLED_LOAD_MORE'
);
expect(container.querySelector('.ais-InfiniteHits-banner')).toHaveClass(
'BANNER_ROOT'
);
expect(
container.querySelector('.ais-InfiniteHits-banner-image')
).toHaveClass('BANNER_IMAGE');
expect(
container.querySelector('.ais-InfiniteHits-banner-link')
).toHaveClass('BANNER_LINK');
});

type CustomRecord = { somethingSpecial: string };
Expand Down Expand Up @@ -274,7 +305,11 @@ describe('infiniteHits', () => {
test('renders with templates using `html`', async () => {
const container = document.createElement('div');
const searchBoxContainer = document.createElement('div');
const searchClient = createMockedSearchClient();
const searchClient = createMockedSearchClient({
// @TODO: remove once algoliasearch js client has been updated
// @ts-expect-error
renderingContent: bannerWidgetRenderingContent,
});

const search = instantsearch({ indexName: 'indexName', searchClient });

Expand Down Expand Up @@ -308,6 +343,11 @@ describe('infiniteHits', () => {
empty({ query }, { html }) {
return html`<p>No results for <q>${query}</q></p>`;
},
banner({ banner, className }, { html }) {
return html`<div class="${className}">
<img src="${banner?.image.urls[0].url}" />
</div>`;
},
},
}),
]);
Expand All @@ -329,6 +369,13 @@ describe('infiniteHits', () => {
Show previous
</span>
</button>
<div
class="ais-InfiniteHits-banner"
>
<img
src="https://via.placeholder.com/550x250"
/>
</div>
<ol
class="ais-InfiniteHits-list"
>
Expand Down Expand Up @@ -512,6 +559,13 @@ describe('infiniteHits', () => {
<div
class="ais-InfiniteHits ais-InfiniteHits--empty"
>
<div
class="ais-InfiniteHits-banner"
>
<img
src="https://via.placeholder.com/550x250"
/>
</div>
<p>
No results for
<q>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,21 @@ export type InfiniteHitsCSSClasses = Partial<{
* The disabled “Show more” button.
*/
disabledLoadMore: string | string[];

/**
* Class names to apply to the banner element
*/
bannerRoot: string | string[];

/**
* Class names to apply to the banner image element
*/
bannerImage: string | string[];

/**
* Class names to apply to the banner link element
*/
bannerLink: string | string[];
}>;

export type InfiniteHitsTemplates<THit extends NonNullable<object> = BaseHit> =
Expand Down Expand Up @@ -110,6 +125,14 @@ export type InfiniteHitsTemplates<THit extends NonNullable<object> = BaseHit> =
__hitIndex: number;
}
>;

/**
* Template to use for the banner.
*/
banner: Template<{
banner: Required<InfiniteHitsRenderState['banner']>;
className: string;
}>;
}>;

export type InfiniteHitsWidgetParams<
Expand Down Expand Up @@ -172,6 +195,7 @@ const renderer =
insights,
bindEvent,
sendEvent,
banner,
},
isFirstRendering
) => {
Expand Down Expand Up @@ -199,6 +223,7 @@ const renderer =
insights={insights as InsightsClient}
sendEvent={sendEvent}
bindEvent={bindEvent}
banner={banner}
/>,
containerNode
);
Expand Down Expand Up @@ -243,6 +268,18 @@ export default (function infiniteHits<
suit({ descendantName: 'loadMore', modifierName: 'disabled' }),
userCssClasses.disabledLoadMore
),
bannerRoot: cx(
suit({ descendantName: 'banner' }),
userCssClasses.bannerRoot
),
bannerImage: cx(
suit({ descendantName: 'banner-image' }),
userCssClasses.bannerImage
),
bannerLink: cx(
suit({ descendantName: 'banner-link' }),
userCssClasses.bannerLink
),
};

const specializedRenderer = renderer({
Expand Down
Loading

0 comments on commit 9287532

Please sign in to comment.