Skip to content
This repository has been archived by the owner on Dec 30, 2022. It is now read-only.

Commit

Permalink
feat(hooks): introduce useBreadcrumb() (#3245)
Browse files Browse the repository at this point in the history
* feat(hooks): introduce `useBreadcrumb`

* chore(hooks): fix breadcrumb example

* test(hooks): add useBreadcrumb test

* chore(hooks): fix useBreadcrumb type check

* chore(hooks): breadcrumb hook example fixes

* chore(hooks): fix breadcrumb typing

* chore(tests): fix typing

* chore(tests): review fixes

* chore(hooks): fix breadcrumb component example

* chore(hooks): use undefined and ignore TS error for now

* chore(tests): fix imports

* chore(hooks): go back with null for now

* chore(hooks): breadcrumb example accessibility fix
  • Loading branch information
FabienMotte committed Jan 25, 2022
1 parent 3e31dc0 commit 5bfbaaf
Show file tree
Hide file tree
Showing 6 changed files with 271 additions and 0 deletions.
9 changes: 9 additions & 0 deletions examples/hooks/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
} from 'react-instantsearch-hooks';

import {
Breadcrumb,
ClearRefinements,
CurrentRefinements,
HierarchicalMenu,
Expand Down Expand Up @@ -106,6 +107,14 @@ export function App() {
</DynamicWidgets>
</div>
<div className="Search">
<Breadcrumb
attributes={[
'hierarchicalCategories.lvl0',
'hierarchicalCategories.lvl1',
'hierarchicalCategories.lvl2',
]}
/>

<div className="Search-header">
<SearchBox placeholder="Search" />
<SortBy
Expand Down
78 changes: 78 additions & 0 deletions examples/hooks/components/Breadcrumb.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import React from 'react';
import { useBreadcrumb, UseBreadcrumbProps } from 'react-instantsearch-hooks';

import { cx } from '../cx';
import { isModifierClick } from '../isModifierClick';

export type BreadcrumbProps = React.ComponentProps<'div'> & UseBreadcrumbProps;

export function Breadcrumb(props: BreadcrumbProps) {
const { items, refine, canRefine, createURL } = useBreadcrumb(props);

return (
<div
className={cx(
'ais-Breadcrumb',
!canRefine && 'ais-Breadcrumb--noRefinement',
props.className
)}
>
<ul className="ais-Breadcrumb-list">
<li
className={cx(
'ais-Breadcrumb-item',
!canRefine && 'ais-Breadcrumb-item--selected'
)}
>
<a
href={createURL(null)}
onClick={(event) => {
if (!isModifierClick(event)) {
event.preventDefault();
refine(null);
}
}}
className="ais-Breadcrumb-link"
>
Home
</a>
</li>

{items.map((item, idx) => {
const isLast = idx === items.length - 1;

return (
<li
key={idx}
className={cx(
'ais-Breadcrumb-item',
isLast && 'ais-Breadcrumb-item--selected'
)}
>
<span aria-hidden="true" className="ais-Breadcrumb-separator">
&gt;
</span>

{isLast ? (
item.label
) : (
<a
className="ais-Breadcrumb-link"
href={createURL(item.value)}
onClick={(event) => {
if (!isModifierClick(event)) {
event.preventDefault();
refine(item.value);
}
}}
>
{item.label}
</a>
)}
</li>
);
})}
</ul>
</div>
);
}
1 change: 1 addition & 0 deletions examples/hooks/components/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export * from './Breadcrumb';
export * from './ClearRefinements';
export * from './CurrentRefinements';
export * from './HierarchicalMenu';
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
import { renderHook } from '@testing-library/react-hooks';
import React from 'react';

import { createSearchClient } from '../../../../../test/mock';
import {
createMultiSearchResponse,
createSingleSearchResponse,
} from '../../../../../test/mock/createAPIResponse';
import { createInstantSearchTestWrapper } from '../../../../../test/utils';
import { useBreadcrumb } from '../useBreadcrumb';
import { useHierarchicalMenu } from '../useHierarchicalMenu';

import type { UseHierarchicalMenuProps } from '../useHierarchicalMenu';

describe('useBreadcrumb', () => {
it('returns the connector render state', async () => {
const wrapper = createInstantSearchTestWrapper();
const { result, waitForNextUpdate } = renderHook(
() =>
useBreadcrumb({
attributes: [
'hierarchicalCategories.lvl0',
'hierarchicalCategories.lvl1',
'hierarchicalCategories.lvl2',
],
}),
{
wrapper,
}
);

// Initial render state from manual `getWidgetRenderState`
expect(result.current).toEqual({
canRefine: false,
createURL: expect.any(Function),
items: [],
refine: expect.any(Function),
});

await waitForNextUpdate();

// InstantSearch.js state from the `render` lifecycle step
expect(result.current).toEqual({
canRefine: false,
createURL: expect.any(Function),
items: [],
refine: expect.any(Function),
});
});

it('returns the connector render state with initial UI state', async () => {
const wrapper = createInstantSearchTestWrapper({
initialUiState: {
indexName: {
hierarchicalMenu: {
'hierarchicalCategories.lvl0': [
'Appliances',
'Small Kitchen Appliances',
'Coffee, Tea & Espresso',
],
},
},
},
searchClient: createSearchClient({
search: jest.fn((requests) =>
Promise.resolve(
createMultiSearchResponse(
...requests.map((request) =>
createSingleSearchResponse({
index: request.indexName,
facets: {
'hierarchicalCategories.lvl0': {
Appliances: 382,
},
'hierarchicalCategories.lvl1': {
'Appliances > Small Kitchen Appliances': 382,
},
'hierarchicalCategories.lvl2': {
'Appliances > Small Kitchen Appliances > Coffee, Tea & Espresso': 382,
},
},
hits: [
{
objectID: '1',
hierarchicalCategories: {
lvl0: 'Appliances',
lvl1: 'Appliances > Small Kitchen Appliances',
lvl2: 'Appliances > Small Kitchen Appliances > Coffee, Tea & Espresso',
},
},
],
})
)
)
)
),
}),
});
const { result, waitForNextUpdate } = renderHook(
() =>
useBreadcrumb({
attributes: [
'hierarchicalCategories.lvl0',
'hierarchicalCategories.lvl1',
'hierarchicalCategories.lvl2',
],
}),
{
wrapper: ({ children }) =>
wrapper({
children: (
<>
<HierarchicalMenu
attributes={[
'hierarchicalCategories.lvl0',
'hierarchicalCategories.lvl1',
'hierarchicalCategories.lvl2',
]}
/>
{children}
</>
),
}),
}
);

// Initial render state from manual `getWidgetRenderState`
expect(result.current).toEqual({
canRefine: false,
createURL: expect.any(Function),
items: [],
refine: expect.any(Function),
});

await waitForNextUpdate();

// InstantSearch.js state from the `render` lifecycle step
expect(result.current).toEqual({
canRefine: true,
createURL: expect.any(Function),
items: [
{
label: 'Appliances',
value: 'Appliances > Small Kitchen Appliances',
},
{
label: 'Small Kitchen Appliances',
value:
'Appliances > Small Kitchen Appliances > Coffee, Tea & Espresso',
},
{
label: 'Coffee, Tea & Espresso',
value: null,
},
],
refine: expect.any(Function),
});
});
});

function HierarchicalMenu(props: UseHierarchicalMenuProps) {
useHierarchicalMenu(props);

return null;
}
17 changes: 17 additions & 0 deletions packages/react-instantsearch-hooks/src/connectors/useBreadcrumb.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import connectBreadcrumb from 'instantsearch.js/es/connectors/breadcrumb/connectBreadcrumb';

import { useConnector } from '../hooks/useConnector';

import type {
BreadcrumbConnectorParams,
BreadcrumbWidgetDescription,
} from 'instantsearch.js/es/connectors/breadcrumb/connectBreadcrumb';

export type UseBreadcrumbProps = BreadcrumbConnectorParams;

export function useBreadcrumb(props: UseBreadcrumbProps) {
return useConnector<BreadcrumbConnectorParams, BreadcrumbWidgetDescription>(
connectBreadcrumb,
props
);
}
1 change: 1 addition & 0 deletions packages/react-instantsearch-hooks/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ export * from './components/Index';
export * from './components/InstantSearch';
export * from './components/InstantSearchServerContext';
export * from './components/InstantSearchSSRProvider';
export * from './connectors/useBreadcrumb';
export * from './connectors/useClearRefinements';
export * from './connectors/useConfigure';
export * from './connectors/useCurrentRefinements';
Expand Down

0 comments on commit 5bfbaaf

Please sign in to comment.