-
Notifications
You must be signed in to change notification settings - Fork 549
feat(autocomplete): implement basic shared DOM #6709
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from 1 commit
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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} | ||
</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)}> | ||
aymeric-giraudet marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
Haroenv marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
{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 |
---|---|---|
|
@@ -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
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We do barrel files in other libraries, think this is more consistent |
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; | ||
|
@@ -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} /> | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. should we cast it instead? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Doesn't work, but it's sensibly the same as adding |
||
ItemComponent={ItemComponent} | ||
items={items} | ||
/> | ||
); | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1 +1,2 @@ | ||
export * from './Carousel'; | ||
export * from './Autocomplete'; |
There was a problem hiding this comment.
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.