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

Commit 0000499

Browse files
feat(core): allow input pause in keyboard navigation
1 parent 0f4101b commit 0000499

9 files changed

Lines changed: 49 additions & 44 deletions

File tree

packages/autocomplete-core/completion.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,9 @@ export function getCompletion<TItem>({
1111
props,
1212
}: GetCompletionProps<TItem>): string | null {
1313
if (
14-
!props.showCompletion ||
15-
state.highlightedIndex < 0 ||
16-
!state.isOpen ||
14+
props.showCompletion === false ||
15+
state.isOpen === false ||
16+
state.highlightedIndex === null ||
1717
state.status === 'stalled'
1818
) {
1919
return null;

packages/autocomplete-core/defaultProps.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ export function getDefaultProps<TItem>(
1818
minLength: 1,
1919
placeholder: '',
2020
autoFocus: false,
21-
defaultHighlightedIndex: 0,
21+
defaultHighlightedIndex: null,
2222
showCompletion: false,
2323
stallThreshold: 300,
2424
environment,
@@ -31,7 +31,7 @@ export function getDefaultProps<TItem>(
3131
id: props.id ?? generateAutocompleteId(),
3232
// The following props need to be deeply defaulted.
3333
initialState: {
34-
highlightedIndex: 0,
34+
highlightedIndex: null,
3535
query: '',
3636
suggestions: [],
3737
isOpen: false,

packages/autocomplete-core/onKeyDown.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ export function onKeyDown<TItem>({
4848
);
4949
nodeItem?.scrollIntoView(false);
5050

51-
if (store.getState().highlightedIndex >= 0) {
51+
if (store.getState().highlightedIndex !== null) {
5252
const { item, itemValue, itemUrl, source } = getHighlightedItem({
5353
state: store.getState(),
5454
});
@@ -76,7 +76,7 @@ export function onKeyDown<TItem>({
7676
(event.target as HTMLInputElement).selectionStart ===
7777
store.getState().query.length)) &&
7878
props.showCompletion &&
79-
store.getState().highlightedIndex >= 0
79+
store.getState().highlightedIndex !== null
8080
) {
8181
event.preventDefault();
8282

@@ -117,7 +117,7 @@ export function onKeyDown<TItem>({
117117
} else if (event.key === 'Enter') {
118118
// No item is selected, so we let the browser handle the native `onSubmit`
119119
// form event.
120-
if (store.getState().highlightedIndex < 0) {
120+
if (store.getState().highlightedIndex === null) {
121121
return;
122122
}
123123

packages/autocomplete-core/propGetters.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -127,7 +127,7 @@ export function getPropGetters<TItem>({
127127
return {
128128
'aria-autocomplete': props.showCompletion ? 'both' : 'list',
129129
'aria-activedescendant':
130-
store.getState().isOpen && store.getState().highlightedIndex >= 0
130+
store.getState().isOpen && store.getState().highlightedIndex !== null
131131
? `${props.id}-item-${store.getState().highlightedIndex}`
132132
: null,
133133
'aria-controls': store.getState().isOpen ? `${props.id}-menu` : null,
@@ -228,7 +228,7 @@ export function getPropGetters<TItem>({
228228
);
229229
props.onStateChange({ state: store.getState() });
230230

231-
if (store.getState().highlightedIndex >= 0) {
231+
if (store.getState().highlightedIndex !== null) {
232232
const { item, itemValue, itemUrl, source } = getHighlightedItem({
233233
state: store.getState(),
234234
});

packages/autocomplete-core/stateReducer.ts

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -81,9 +81,10 @@ export const stateReducer = <TItem>(
8181
return {
8282
...state,
8383
highlightedIndex: getNextHighlightedIndex(
84-
action.value.shiftKey ? 5 : 1,
84+
1,
8585
state.highlightedIndex,
86-
getItemsCount(state)
86+
getItemsCount(state),
87+
props.defaultHighlightedIndex
8788
),
8889
};
8990
}
@@ -92,9 +93,10 @@ export const stateReducer = <TItem>(
9293
return {
9394
...state,
9495
highlightedIndex: getNextHighlightedIndex(
95-
action.value.shiftKey ? -5 : -1,
96+
-1,
9697
state.highlightedIndex,
97-
getItemsCount(state)
98+
getItemsCount(state),
99+
props.defaultHighlightedIndex
98100
),
99101
};
100102
}
@@ -119,7 +121,7 @@ export const stateReducer = <TItem>(
119121
case 'submit': {
120122
return {
121123
...state,
122-
highlightedIndex: -1,
124+
highlightedIndex: null,
123125
isOpen: false,
124126
status: 'idle',
125127
statusContext: {},
@@ -129,7 +131,7 @@ export const stateReducer = <TItem>(
129131
case 'reset': {
130132
return {
131133
...state,
132-
highlightedIndex: -1,
134+
highlightedIndex: null,
133135
isOpen: false,
134136
status: 'idle',
135137
statusContext: {},
@@ -151,7 +153,7 @@ export const stateReducer = <TItem>(
151153
return {
152154
...state,
153155
isOpen: __DEV__ ? state.isOpen : false,
154-
highlightedIndex: -1,
156+
highlightedIndex: null,
155157
};
156158
}
157159

packages/autocomplete-core/types/api.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -155,9 +155,9 @@ export interface PublicAutocompleteOptions<TItem> {
155155
/**
156156
* The default item index to pre-select.
157157
*
158-
* @default 0
158+
* @default null
159159
*/
160-
defaultHighlightedIndex?: number;
160+
defaultHighlightedIndex?: number | null;
161161
/**
162162
* Whether to show the highlighted suggestion as completion in the input.
163163
*
@@ -217,7 +217,7 @@ export interface AutocompleteOptions<TItem> {
217217
onStateChange<TItem>(props: { state: AutocompleteState<TItem> }): void;
218218
placeholder: string;
219219
autoFocus: boolean;
220-
defaultHighlightedIndex: number;
220+
defaultHighlightedIndex: number | null;
221221
showCompletion: boolean;
222222
minLength: number;
223223
stallThreshold: number;

packages/autocomplete-core/types/state.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { AutocompleteSuggestion } from './api';
22

33
export interface AutocompleteState<TItem> {
4-
highlightedIndex: number;
4+
highlightedIndex: number | null;
55
query: string;
66
suggestions: Array<AutocompleteSuggestion<TItem>>;
77
isOpen: boolean;

packages/autocomplete-core/utils.ts

Lines changed: 25 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import {
55
AutocompleteSource,
66
GetSources,
77
AutocompleteSuggestion,
8+
AutocompleteOptions,
89
} from './types';
910

1011
export const noop = () => {};
@@ -70,30 +71,34 @@ export function normalizeGetSources<TItem>(
7071
};
7172
}
7273

73-
export function getNextHighlightedIndex(
74+
export function getNextHighlightedIndex<TItem>(
7475
moveAmount: number,
75-
baseIndex: number,
76-
itemCount: number
77-
) {
78-
const itemsLastIndex = itemCount - 1;
79-
80-
if (
81-
typeof baseIndex !== 'number' ||
82-
baseIndex < 0 ||
83-
baseIndex >= itemCount
84-
) {
85-
baseIndex = moveAmount > 0 ? -1 : itemsLastIndex + 1;
76+
baseIndex: number | null,
77+
itemCount: number,
78+
defaultHighlightedIndex: AutocompleteOptions<TItem>['defaultHighlightedIndex']
79+
): number | null {
80+
// We allow circular keyboard navigation from the base index.
81+
// The base index can either be `null` (nothing is highlighted) or `0`
82+
// (the first item is highlighted).
83+
// The base index is allowed to get assigned `null` only if
84+
// `props.defaultHighlightedIndex` is `null`. This pattern allows to "stop"
85+
// by the actual query before navigating to other suggestions as seen on
86+
// Google or Amazon.
87+
if (baseIndex === null && moveAmount < 0) {
88+
return itemCount - 1;
8689
}
8790

88-
let newIndex = baseIndex + moveAmount;
91+
if (defaultHighlightedIndex !== null && baseIndex === 0 && moveAmount < 0) {
92+
return itemCount - 1;
93+
}
94+
95+
const numericIndex = (baseIndex === null ? -1 : baseIndex) + moveAmount;
8996

90-
if (newIndex < 0) {
91-
newIndex = itemsLastIndex;
92-
} else if (newIndex > itemsLastIndex) {
93-
newIndex = 0;
97+
if (numericIndex <= -1 || numericIndex >= itemCount) {
98+
return defaultHighlightedIndex === null ? null : 0;
9499
}
95100

96-
return newIndex;
101+
return numericIndex;
97102
}
98103

99104
// We don't have access to the autocomplete source when we call `onKeyDown`
@@ -120,7 +125,7 @@ function getSuggestionFromHighlightedIndex<TItem>({
120125

121126
// Based on the accumulated counts, we can infer the index of the suggestion.
122127
const suggestionIndex = accumulatedSuggestionsCount.reduce((acc, current) => {
123-
if (current <= state.highlightedIndex) {
128+
if (current <= state.highlightedIndex!) {
124129
return acc + 1;
125130
}
126131

@@ -164,7 +169,7 @@ function getRelativeHighlightedIndex<TItem>({
164169
counter++;
165170
}
166171

167-
return state.highlightedIndex - previousItemsOffset;
172+
return state.highlightedIndex! - previousItemsOffset;
168173
}
169174

170175
export function getHighlightedItem<TItem>({

stories/react.stories.tsx

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,8 @@ storiesOf('React', module)
2121
<Autocomplete
2222
placeholder="Search items…"
2323
showCompletion={true}
24-
defaultHighlightedIndex={-1}
2524
dropdownContainer={dropdownContainer}
25+
defaultHighlightedIndex={null}
2626
getSources={() => {
2727
return [
2828
{
@@ -60,7 +60,6 @@ storiesOf('React', module)
6060
<Autocomplete
6161
placeholder="Search items…"
6262
showCompletion={true}
63-
defaultHighlightedIndex={-1}
6463
dropdownContainer={dropdownContainer}
6564
getSources={() => {
6665
return [
@@ -101,7 +100,6 @@ storiesOf('React', module)
101100
render(
102101
<Autocomplete
103102
placeholder="Search items…"
104-
defaultHighlightedIndex={-1}
105103
dropdownContainer={dropdownContainer}
106104
getSources={() => {
107105
return [

0 commit comments

Comments
 (0)