Skip to content
This repository has been archived by the owner on Jun 11, 2021. It is now read-only.

feat(react): place dropdown with Popper #25

Merged
merged 4 commits into from
Feb 14, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
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
1 change: 1 addition & 0 deletions .storybook/static/autocomplete.css
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,7 @@
padding: 0.5rem;
scrollbar-width: none;
width: 100%;
margin-top: 5px;
}

.algolia-autocomplete-dropdown::-webkit-scrollbar {
Expand Down
2 changes: 1 addition & 1 deletion packages/autocomplete-core/types/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ export type GetSources<TItem> = (
options: GetSourcesOptions<TItem>
) => Promise<Array<AutocompleteSource<TItem>>>;

interface Environment {
export interface Environment {
[prop: string]: unknown;
addEventListener: Window['addEventListener'];
removeEventListener: Window['removeEventListener'];
Expand Down
96 changes: 90 additions & 6 deletions packages/autocomplete-react/Autocomplete.tsx
Original file line number Diff line number Diff line change
@@ -1,29 +1,71 @@
/** @jsx h */

import { h } from 'preact';
import { useRef, useState } from 'preact/hooks';
import { useRef, useState, useLayoutEffect } from 'preact/hooks';
import { createPortal } from 'preact/compat';
import { createPopper } from '@popperjs/core/lib/popper-lite';

import { createAutocomplete } from '../autocomplete-core';
import { getDefaultProps } from '../autocomplete-core/defaultProps';
import { getHTMLElement } from './getHTMLElement';
import { SearchBox } from './SearchBox';
import { Dropdown } from './Dropdown';

import {
PublicAutocompleteOptions,
AutocompleteState,
AutocompleteOptions,
} from '../autocomplete-core/types/index';

interface PublicRendererProps {
dropdownContainer?: string | HTMLElement;
dropdownPlacement?: 'start' | 'end';
}

interface RendererProps extends Required<PublicRendererProps> {
dropdownContainer: HTMLElement;
}

interface PublicProps<TItem>
extends AutocompleteOptions<TItem>,
RendererProps {}

export function getDefaultRendererProps(
rendererProps: PublicRendererProps,
autocompleteProps: AutocompleteOptions<any>
): RendererProps {
return {
dropdownContainer: rendererProps.dropdownContainer
? getHTMLElement(
rendererProps.dropdownContainer,
autocompleteProps.environment
)
: autocompleteProps.environment.document.body,
dropdownPlacement: rendererProps.dropdownPlacement ?? 'start',
};
}

export function Autocomplete<TItem extends {}>(
providedProps: PublicAutocompleteOptions<TItem>
providedProps: PublicProps<TItem>
) {
const props = getDefaultProps(providedProps);
const {
dropdownContainer,
dropdownPlacement,
...autocompleteProps
} = providedProps;
const props = getDefaultProps(autocompleteProps);
const rendererProps = getDefaultRendererProps(
{ dropdownContainer, dropdownPlacement },
props
);

const [state, setState] = useState<AutocompleteState<TItem>>(
props.initialState
);

const inputRef = useRef<HTMLInputElement | null>(null);
const searchBoxRef = useRef<HTMLFormElement | null>(null);
const dropdownRef = useRef<HTMLDivElement | null>(null);
const popper = useRef<ReturnType<typeof createPopper> | null>(null);
const autocomplete = useRef(
createAutocomplete<TItem>({
...props,
Expand All @@ -35,6 +77,45 @@ export function Autocomplete<TItem extends {}>(
})
);

useLayoutEffect(() => {
if (searchBoxRef.current && dropdownRef.current) {
popper.current = createPopper(searchBoxRef.current, dropdownRef.current, {
placement:
rendererProps.dropdownPlacement === 'end'
? 'bottom-end'
: 'bottom-start',
modifiers: [
// By default, Popper overrides the `margin` style to `0` because it
// is known to cause issues when computing the position.
// We consider this as a problem in Autocomplete because it prevents
// users from setting different desktop/mobile styles in CSS.
// If we leave Popper override `margin`, users would have to use the
// `!important` CSS keyword or we would have to expose a JavaScript
// API.
// See https://github.com/francoischalifour/autocomplete.js/pull/25
{
name: 'unsetMargins',
enabled: true,
fn: ({ state }) => {
state.styles.popper.margin = '';
},
requires: ['computeStyles'],
phase: 'beforeWrite',
},
],
});
}

return () => {
popper.current?.destroy();
};
}, [searchBoxRef, dropdownRef, rendererProps.dropdownPlacement]);

useLayoutEffect(() => {
if (state.isOpen) {
popper.current?.update();
}
}, [state.isOpen]);

return (
<div
Expand All @@ -48,6 +129,7 @@ export function Autocomplete<TItem extends {}>(
{...autocomplete.current.getRootProps()}
>
<SearchBox
searchBoxRef={searchBoxRef}
inputRef={inputRef}
placeholder={props.placeholder}
query={state.query}
Expand All @@ -61,14 +143,16 @@ export function Autocomplete<TItem extends {}>(
})}
/>

{state.isOpen && (
{createPortal(
<Dropdown
dropdownRef={dropdownRef}
suggestions={state.suggestions}
isOpen={state.isOpen}
status={state.status}
getItemProps={autocomplete.current.getItemProps}
getMenuProps={autocomplete.current.getMenuProps}
/>
/>,
rendererProps.dropdownContainer
)}
</div>
);
Expand Down
3 changes: 3 additions & 0 deletions packages/autocomplete-react/Dropdown.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
/** @jsx h */

import { h } from 'preact';
import { Ref } from 'preact/compat';

import { reverseHighlightAlgoliaHit } from '../autocomplete-presets';

Expand All @@ -16,6 +17,7 @@ interface DropdownProps {
suggestions: Array<AutocompleteSuggestion<any>>;
getItemProps: GetItemProps<any>;
getMenuProps: GetMenuProps;
dropdownRef: Ref<HTMLDivElement | null>;
}

export const Dropdown = (props: DropdownProps) => {
Expand All @@ -28,6 +30,7 @@ export const Dropdown = (props: DropdownProps) => {
]
.filter(Boolean)
.join(' ')}
ref={props.dropdownRef}
hidden={!props.isOpen}
>
{props.isOpen && (
Expand Down
2 changes: 2 additions & 0 deletions packages/autocomplete-react/SearchBox.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ export interface SearchBoxProps {
getInputProps: GetInputProps;
getLabelProps: GetLabelProps;
inputRef: Ref<HTMLInputElement>;
searchBoxRef: Ref<HTMLFormElement>;
}

export function SearchBox(props: SearchBoxProps) {
Expand All @@ -26,6 +27,7 @@ export function SearchBox(props: SearchBoxProps) {
className="algolia-autocomplete-form"
onSubmit={props.onSubmit}
onReset={props.onReset}
ref={props.searchBoxRef}
>
<label
className="algolia-autocomplete-magnifierLabel"
Expand Down
12 changes: 12 additions & 0 deletions packages/autocomplete-react/getHTMLElement.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { Environment } from '../autocomplete-core/types';

export function getHTMLElement(
value: string | HTMLElement,
environment: Environment
): HTMLElement {
if (typeof value === 'string') {
return environment.document.querySelector<HTMLElement>(value)!;
}

return value;
}
3 changes: 2 additions & 1 deletion packages/autocomplete-react/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
"dist/"
],
"dependencies": {
"preact": "^10.3.1"
"@popperjs/core": "^2.0.0",
"preact": "^10.0.0"
}
}
27 changes: 20 additions & 7 deletions stories/display.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,10 @@ const searchClient = algoliasearch(
'6be0576ff61c053d5f9a3225e2a90f76'
);

function Component() {
function Component(props) {
return (
<Autocomplete
{...props}
getSources={() => {
return [
{
Expand Down Expand Up @@ -46,17 +47,23 @@ function Component() {
storiesOf('Display', module)
.add(
'Heading search bar',
withPlayground(({ container }) => {
render(<Component />, container);
withPlayground(({ container, dropdownContainer }) => {
render(<Component dropdownContainer={dropdownContainer} />, container);

return container;
})
)
.add(
'Left search bar',
withPlayground(
({ container }) => {
render(<Component />, container);
({ container, dropdownContainer }) => {
render(
<Component
dropdownContainer={dropdownContainer}
dropdownPlacement="start"
/>,
container
);

return container;
},
Expand All @@ -68,8 +75,14 @@ storiesOf('Display', module)
.add(
'Right search bar',
withPlayground(
({ container }) => {
render(<Component />, container);
({ container, dropdownContainer }) => {
render(
<Component
dropdownContainer={dropdownContainer}
dropdownPlacement="end"
/>,
container
);

return container;
},
Expand Down
3 changes: 2 additions & 1 deletion stories/react.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,13 @@ const searchClient = algoliasearch(

storiesOf('React', module).add(
'Component',
withPlayground(({ container }) => {
withPlayground(({ container, dropdownContainer }) => {
render(
<Autocomplete
placeholder="Search items…"
showCompletion={true}
defaultHighlightedIndex={-1}
dropdownContainer={dropdownContainer}
getSources={() => {
return [
{
Expand Down
Loading