Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion bundlesize.config.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
},
{
"path": "packages/react-instantsearch/dist/umd/ReactInstantSearch.min.js",
"maxSize": "66.5 kB"
"maxSize": "66.75 kB"
},
{
"path": "packages/vue-instantsearch/vue2/umd/index.js",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
/** @jsx createElement */

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

import type { ComponentChildren, Renderer } from '../../types';

export type AutocompleteProps = {
isOpen: boolean;
children?: ComponentChildren;
classNames?: Partial<AutocompleteClassNames>;
};

export type AutocompleteClassNames = {
/**
* Class names to apply to the root element
*/
root: string | string[];
};

export function createAutocompleteComponent({ createElement }: Renderer) {
return function Autocomplete(userProps: AutocompleteProps) {
const { children, isOpen, classNames = {} } = userProps;

return (
<div
className={cx('ais-Autocomplete', classNames.root)}
role="combobox"
aria-expanded={isOpen}
aria-haspopup="listbox"
>
{children}
Copy link
Contributor

Choose a reason for hiding this comment

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

in a later iteration (when we do the final dom) we likely want to have the search box be part of this too, and then all the intermediate components also be part of the autocomplete. For now this is a logical split.

</div>
);
};
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
/** @jsx createElement */

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

import type { Renderer } from '../../types';

export type AutocompleteIndexProps<
T = { objectID: string } & Record<string, unknown>
> = {
items: T[];
ItemComponent: (item: T) => JSX.Element;
classNames?: Partial<AutocompleteIndexClassNames>;
};

export type AutocompleteIndexClassNames = {
/**
* Class names to apply to the root element
**/
root: string | string[];
/**
* Class names to apply to the list element
*/
list: string | string[];
/**
* Class names to apply to each item element
*/
item: string | string[];
};

export function createAutocompleteIndexComponent({ createElement }: Renderer) {
return function AutocompleteIndex(userProps: AutocompleteIndexProps) {
const { items, ItemComponent, classNames = {} } = userProps;

return (
<div className={cx('ais-AutocompleteIndex', classNames.root)}>
<ul className={cx('ais-AutocompleteIndexList', classNames.list)}>
{items.map((item) => (
<li
key={item.objectID}
className={cx('ais-AutocompleteIndexItem', classNames.item)}
>
<ItemComponent {...item} />
</li>
))}
</ul>
</div>
);
};
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
/** @jsx createElement */

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

import type { ComponentChildren, Renderer } from '../../types';

export type AutocompletePanelProps = {
isOpen: boolean;
children?: ComponentChildren;
classNames?: Partial<AutocompletePanelClassNames>;
};

export type AutocompletePanelClassNames = {
/**
* Class names to apply to the root element
*/
root: string | string[];
/**
* Class names to apply to the layout element
*/
layout: string | string[];
};

export function createAutocompletePanelComponent({ createElement }: Renderer) {
return function AutocompletePanel(userProps: AutocompletePanelProps) {
const { children, isOpen, classNames = {} } = userProps;

return (
<div
className={cx('ais-AutocompletePanel', classNames.root)}
hidden={!isOpen}
>
<div className={cx('ais-AutocompletePanelLayout', classNames.layout)}>
{children}
</div>
</div>
);
};
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export * from './Autocomplete';
export * from './AutocompleteIndex';
export * from './AutocompletePanel';
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,4 @@ export * from './chat/ChatMessage';
export * from './chat/ChatToggleButton';
export * from './chat/ChatHeader';
export * from './chat/types';
export * from './autocomplete';
Comment on lines 13 to +15
Copy link
Contributor

Choose a reason for hiding this comment

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

not consistent between chat (no barrel file) and autocomplete (barrel file). I don't have a specific preference, but we'll want to consolidate after both sprints are over.

Copy link
Member Author

Choose a reason for hiding this comment

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

We do barrel files in other libraries, think this is more consistent

63 changes: 54 additions & 9 deletions packages/react-instantsearch/src/components/Autocomplete.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,31 @@
import React from 'react';
import { Index } from 'react-instantsearch-core';
import {
createAutocompleteComponent,
createAutocompleteIndexComponent,
createAutocompletePanelComponent,
} from 'instantsearch-ui-components';
import React, { createElement, useState, Fragment } from 'react';
import { Index, useHits } from 'react-instantsearch-core';

import { Hits, SearchBox } from '../widgets';
import { SearchBox } from '../widgets';

import type { Pragma } from 'instantsearch-ui-components';
import type { BaseHit, Hit } from 'instantsearch.js';

const Autocomplete = createAutocompleteComponent({
createElement: createElement as Pragma,
Fragment,
});

const AutocompletePanel = createAutocompletePanelComponent({
createElement: createElement as Pragma,
Fragment,
});

const AutocompleteIndex = createAutocompleteIndexComponent({
createElement: createElement as Pragma,
Fragment,
});

type IndexConfig<TItem extends Hit<BaseHit> = Hit<BaseHit>> = {
indexName: string;
getQuery?: (item: TItem) => string;
Expand All @@ -17,14 +38,38 @@ export type AutocompleteProps = {
};

export function EXPERIMENTAL_Autocomplete({ indices }: AutocompleteProps) {
const [isOpen, setIsOpen] = useState(false);

return (
<Index EXPERIMENTAL_isolated>
<SearchBox />
{indices.map((index) => (
<Index key={index.indexName} indexName={index.indexName}>
<Hits hitComponent={({ hit }) => <index.itemComponent {...hit} />} />
</Index>
))}
<Autocomplete isOpen={isOpen}>
<SearchBox onFocus={() => setIsOpen(true)} />
<AutocompletePanel isOpen={isOpen}>
{indices.map((index) => (
<Index key={index.indexName} indexName={index.indexName}>
<AutocompleteIndexComponent itemComponent={index.itemComponent} />
Copy link
Contributor

Choose a reason for hiding this comment

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

ok for now, but later once we do keyboard, we likely will want it to be a flat list of indices and use connectAutocomplete? not sure tbh but it likely doesn't change much

</Index>
))}
</AutocompletePanel>
</Autocomplete>
</Index>
);
}

type AutocompleteIndexProps = {
itemComponent: IndexConfig['itemComponent'];
};

function AutocompleteIndexComponent({
itemComponent: ItemComponent,
}: AutocompleteIndexProps) {
const { items } = useHits();

return (
<AutocompleteIndex
// @ts-expect-error - there seems to be problems with React.ComponentType and this, but it's actually correct
Copy link
Contributor

Choose a reason for hiding this comment

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

should we cast it instead?

Copy link
Member Author

Choose a reason for hiding this comment

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

Doesn't work, but it's sensibly the same as adding ts-expect-error (except if the line had more statements than this)

ItemComponent={ItemComponent}
items={items}
/>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ describe('Autocomplete', () => {

test('should render a searchbox and indices with hits', async () => {
const searchClient = createMockedSearchClient();
render(
const { container } = render(
<InstantSearchTestWrapper searchClient={searchClient}>
<EXPERIMENTAL_Autocomplete
indices={[
Expand All @@ -52,8 +52,152 @@ describe('Autocomplete', () => {
</InstantSearchTestWrapper>
);

expect(await screen.findByText('Item 1')).toBeInTheDocument();
expect(await screen.findByText('hello')).toBeInTheDocument();
expect(screen.getByRole('searchbox')).toBeInTheDocument();
await screen.findByText('Item 1');

expect(container.firstChild).toMatchInlineSnapshot(`
<div
aria-expanded="false"
aria-haspopup="listbox"
class="ais-Autocomplete"
role="combobox"
>
<div
class="ais-SearchBox"
>
<form
action=""
class="ais-SearchBox-form"
novalidate=""
role="search"
>
<input
aria-label="Search"
autocapitalize="off"
autocomplete="off"
autocorrect="off"
class="ais-SearchBox-input"
maxlength="512"
placeholder=""
spellcheck="false"
type="search"
value=""
/>
<button
class="ais-SearchBox-submit"
title="Submit the search query"
type="submit"
>
<svg
aria-hidden="true"
class="ais-SearchBox-submitIcon"
height="10"
viewBox="0 0 40 40"
width="10"
>
<path
d="M26.804 29.01c-2.832 2.34-6.465 3.746-10.426 3.746C7.333 32.756 0 25.424 0 16.378 0 7.333 7.333 0 16.378 0c9.046 0 16.378 7.333 16.378 16.378 0 3.96-1.406 7.594-3.746 10.426l10.534 10.534c.607.607.61 1.59-.004 2.202-.61.61-1.597.61-2.202.004L26.804 29.01zm-10.426.627c7.323 0 13.26-5.936 13.26-13.26 0-7.32-5.937-13.257-13.26-13.257C9.056 3.12 3.12 9.056 3.12 16.378c0 7.323 5.936 13.26 13.258 13.26z"
/>
</svg>
</button>
<button
class="ais-SearchBox-reset"
hidden=""
title="Clear the search query"
type="reset"
>
<svg
aria-hidden="true"
class="ais-SearchBox-resetIcon"
height="10"
viewBox="0 0 20 20"
width="10"
>
<path
d="M8.114 10L.944 2.83 0 1.885 1.886 0l.943.943L10 8.113l7.17-7.17.944-.943L20 1.886l-.943.943-7.17 7.17 7.17 7.17.943.944L18.114 20l-.943-.943-7.17-7.17-7.17 7.17-.944.943L0 18.114l.943-.943L8.113 10z"
/>
</svg>
</button>
<span
class="ais-SearchBox-loadingIndicator"
hidden=""
>
<svg
aria-hidden="true"
aria-label="Results are loading"
class="ais-SearchBox-loadingIcon"
height="16"
stroke="#444"
viewBox="0 0 38 38"
width="16"
>
<g
fill="none"
fill-rule="evenodd"
>
<g
stroke-width="2"
transform="translate(1 1)"
>
<circle
cx="18"
cy="18"
r="18"
stroke-opacity=".5"
/>
<path
d="M36 18c0-9.94-8.06-18-18-18"
>
<animatetransform
attributeName="transform"
dur="1s"
from="0 18 18"
repeatCount="indefinite"
to="360 18 18"
type="rotate"
/>
</path>
</g>
</g>
</svg>
</span>
</form>
</div>
<div
class="ais-AutocompletePanel"
hidden=""
>
<div
class="ais-AutocompletePanelLayout"
>
<div
class="ais-AutocompleteIndex"
>
<ul
class="ais-AutocompleteIndexList"
>
<li
class="ais-AutocompleteIndexItem"
>
Item 1
</li>
</ul>
</div>
<div
class="ais-AutocompleteIndex"
>
<ul
class="ais-AutocompleteIndexList"
>
<li
class="ais-AutocompleteIndexItem"
>
hello
</li>
</ul>
</div>
</div>
</div>
</div>
`);
});
});
1 change: 1 addition & 0 deletions packages/react-instantsearch/src/components/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export * from './Carousel';
export * from './Autocomplete';