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

Commit

Permalink
feat(core): support onHighlight on sources
Browse files Browse the repository at this point in the history
  • Loading branch information
francoischalifour committed Feb 14, 2020
1 parent 0cf0a93 commit 0f4101b
Show file tree
Hide file tree
Showing 6 changed files with 146 additions and 53 deletions.
23 changes: 4 additions & 19 deletions packages/autocomplete-core/completion.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,5 @@
import { AutocompleteState, AutocompleteOptions } from './types';
import {
getSuggestionFromHighlightedIndex,
getRelativeHighlightedIndex,
} from './utils';
import { getHighlightedItem } from './utils';

interface GetCompletionProps<TItem> {
state: AutocompleteState<TItem>;
Expand All @@ -22,25 +19,13 @@ export function getCompletion<TItem>({
return null;
}

const suggestion = getSuggestionFromHighlightedIndex({ state });

if (!suggestion) {
return null;
}

const item =
suggestion.items[getRelativeHighlightedIndex({ state, suggestion })];
const inputValue = suggestion.source.getInputValue({
suggestion: item,
state,
});
const { itemValue } = getHighlightedItem({ state });

// The completion should appear only if the _first_ characters of the query
// match with the suggestion.
if (
state.query.length > 0 &&
inputValue.toLocaleLowerCase().indexOf(state.query.toLocaleLowerCase()) ===
0
itemValue.toLocaleLowerCase().indexOf(state.query.toLocaleLowerCase()) === 0
) {
// If the query typed has a different case than the suggestion, we want
// to show the completion matching the case of the query. This makes both
Expand All @@ -49,7 +34,7 @@ export function getCompletion<TItem>({
// - query: 'Gui'
// - suggestion: 'guitar'
// => completion: 'Guitar'
const completion = state.query + inputValue.slice(state.query.length);
const completion = state.query + itemValue.slice(state.query.length);

if (completion === state.query) {
return null;
Expand Down
60 changes: 33 additions & 27 deletions packages/autocomplete-core/onKeyDown.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,7 @@
import { stateReducer } from './stateReducer';
import { onInput } from './onInput';
import { getCompletion } from './completion';
import {
getSuggestionFromHighlightedIndex,
getRelativeHighlightedIndex,
} from './utils';
import { getHighlightedItem } from './utils';

import {
AutocompleteStore,
Expand Down Expand Up @@ -50,6 +47,27 @@ export function onKeyDown<TItem>({
`${props.id}-item-${store.getState().highlightedIndex}`
);
nodeItem?.scrollIntoView(false);

if (store.getState().highlightedIndex >= 0) {
const { item, itemValue, itemUrl, source } = getHighlightedItem({
state: store.getState(),
});

source.onHighlight({
suggestion: item,
suggestionValue: itemValue,
suggestionUrl: itemUrl,
source,
state: store.getState(),
setHighlightedIndex,
setQuery,
setSuggestions,
setIsOpen,
setStatus,
setContext,
event,
});
}
} else if (
(event.key === 'Tab' ||
// When the user hits the right arrow and is at the end of the input
Expand Down Expand Up @@ -97,30 +115,17 @@ export function onKeyDown<TItem>({
);
props.onStateChange({ state: store.getState() });
} else if (event.key === 'Enter') {
// No item is selected, so we let the browser handle the native `onSubmit`
// form event.
if (store.getState().highlightedIndex < 0) {
return;
}

const suggestion = getSuggestionFromHighlightedIndex({
state: store.getState(),
});

const item =
suggestion.items[
getRelativeHighlightedIndex({ state: store.getState(), suggestion })
];

if (item) {
// This prevents the `onSubmit` event to be sent when an item is selected.
event.preventDefault();
}
// This prevents the `onSubmit` event to be sent because an item is
// highlighted.
event.preventDefault();

const itemUrl = suggestion.source.getSuggestionUrl({
suggestion: item,
state: store.getState(),
});
const inputValue = suggestion.source.getInputValue({
suggestion: item,
const { item, itemValue, itemUrl, source } = getHighlightedItem({
state: store.getState(),
});

Expand All @@ -144,7 +149,7 @@ export function onKeyDown<TItem>({
// Keep native browser behavior
} else {
onInput({
query: inputValue,
query: itemValue,
store,
props,
setHighlightedIndex,
Expand All @@ -157,18 +162,19 @@ export function onKeyDown<TItem>({
isOpen: false,
},
}).then(() => {
suggestion.source.onSelect({
source.onSelect({
suggestion: item,
suggestionValue: inputValue,
suggestionValue: itemValue,
suggestionUrl: itemUrl,
source: suggestion.source,
source,
state: store.getState(),
setHighlightedIndex,
setQuery,
setSuggestions,
setIsOpen,
setStatus,
setContext,
event,
});

props.onStateChange({ state: store.getState() });
Expand Down
26 changes: 24 additions & 2 deletions packages/autocomplete-core/propGetters.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { stateReducer } from './stateReducer';
import { onInput } from './onInput';
import { onKeyDown } from './onKeyDown';
import { isSpecialClick } from './utils';
import { isSpecialClick, getHighlightedItem } from './utils';

import {
GetRootProps,
Expand Down Expand Up @@ -211,7 +211,7 @@ export function getPropGetters<TItem>({
role: 'option',
'aria-selected':
store.getState().highlightedIndex === item.__autocomplete_id,
onMouseMove() {
onMouseMove(event) {
if (item.__autocomplete_id === store.getState().highlightedIndex) {
return;
}
Expand All @@ -227,6 +227,27 @@ export function getPropGetters<TItem>({
)
);
props.onStateChange({ state: store.getState() });

if (store.getState().highlightedIndex >= 0) {
const { item, itemValue, itemUrl, source } = getHighlightedItem({
state: store.getState(),
});

source.onHighlight({
suggestion: item,
suggestionValue: itemValue,
suggestionUrl: itemUrl,
source,
state: store.getState(),
setHighlightedIndex,
setQuery,
setSuggestions,
setIsOpen,
setStatus,
setContext,
event,
});
}
},
onMouseDown(event: MouseEvent) {
// Prevents the `activeElement` from being changed to the item so it
Expand Down Expand Up @@ -273,6 +294,7 @@ export function getPropGetters<TItem>({
setIsOpen,
setStatus,
setContext,
event,
});

props.onStateChange({ state: store.getState() });
Expand Down
16 changes: 13 additions & 3 deletions packages/autocomplete-core/types/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,11 +26,14 @@ interface ItemParams<TItem> {
}

interface OnSelectParams<TItem>
extends AutocompleteSetters<TItem>,
ItemParams<TItem> {
extends ItemParams<TItem>,
AutocompleteSetters<TItem> {
state: AutocompleteState<TItem>;
event: Event;
}

type OnHighlightParams<TItem> = OnSelectParams<TItem>;

interface OnSubmitParams<TItem> extends AutocompleteSetters<TItem> {
state: AutocompleteState<TItem>;
event: Event;
Expand Down Expand Up @@ -67,9 +70,16 @@ export interface PublicAutocompleteSource<TItem> {
| Array<AutocompleteSuggestion<TItem>>
| Promise<Array<AutocompleteSuggestion<TItem>>>;
/**
* Called when an item is selected.
* Function called when an item is selected.
*/
onSelect?(params: OnSelectParams<TItem>): void;
/**
* Function called when an item is highlighted.
*
* An item is highlighted either via keyboard navigation or via mouse over.
* You can trigger different behaviors based on the event `type`.
*/
onHighlight?(params: OnHighlightParams<TItem>): void;
}

export type AutocompleteSource<TItem> = {
Expand Down
31 changes: 29 additions & 2 deletions packages/autocomplete-core/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ function normalizeSource<TItem>(
onSelect({ setIsOpen }) {
setIsOpen(false);
},
onHighlight: noop,
...source,
};
}
Expand Down Expand Up @@ -98,7 +99,7 @@ export function getNextHighlightedIndex(
// We don't have access to the autocomplete source when we call `onKeyDown`
// or `onClick` because those are native browser events.
// However, we can get the source from the suggestion index.
export function getSuggestionFromHighlightedIndex<TItem>({
function getSuggestionFromHighlightedIndex<TItem>({
state,
}: {
state: AutocompleteState<TItem>;
Expand Down Expand Up @@ -139,7 +140,13 @@ export function getSuggestionFromHighlightedIndex<TItem>({
* (absolute: 3, relative: 1)
* @param param0
*/
export function getRelativeHighlightedIndex({ state, suggestion }): number {
function getRelativeHighlightedIndex<TItem>({
state,
suggestion,
}: {
state: AutocompleteState<TItem>;
suggestion: AutocompleteSuggestion<TItem>;
}): number {
let isOffsetFound = false;
let counter = 0;
let previousItemsOffset = 0;
Expand All @@ -159,3 +166,23 @@ export function getRelativeHighlightedIndex({ state, suggestion }): number {

return state.highlightedIndex - previousItemsOffset;
}

export function getHighlightedItem<TItem>({
state,
}: {
state: AutocompleteState<TItem>;
}) {
const suggestion = getSuggestionFromHighlightedIndex({ state });
const item =
suggestion.items[getRelativeHighlightedIndex({ state, suggestion })];
const source = suggestion.source;
const itemValue = source.getInputValue({ suggestion: item, state });
const itemUrl = source.getSuggestionUrl({ suggestion: item, state });

return {
item,
itemValue,
itemUrl,
source,
};
}
43 changes: 43 additions & 0 deletions stories/react.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,49 @@ storiesOf('React', module)
container
);

return container;
})
)
.add(
'Replaces query onHighlight',
withPlayground(({ container, dropdownContainer }) => {
render(
<Autocomplete
placeholder="Search items…"
defaultHighlightedIndex={-1}
dropdownContainer={dropdownContainer}
getSources={() => {
return [
{
getInputValue({ suggestion }) {
return suggestion.query;
},
onHighlight({ suggestionValue, setQuery, event }) {
if (event.type === 'keydown') {
setQuery(suggestionValue);
}
},
getSuggestions({ query }) {
return getAlgoliaHits({
searchClient,
queries: [
{
indexName: 'instant_search_demo_query_suggestions',
query,
params: {
hitsPerPage: 4,
},
},
],
});
},
},
];
}}
/>,
container
);

return container;
})
);

0 comments on commit 0f4101b

Please sign in to comment.