From 000049971884e958e92cbfd14331ab88ff2b5e1f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Chalifour?= Date: Fri, 14 Feb 2020 14:29:44 +0100 Subject: [PATCH] feat(core): allow input pause in keyboard navigation --- packages/autocomplete-core/completion.ts | 6 +-- packages/autocomplete-core/defaultProps.ts | 4 +- packages/autocomplete-core/onKeyDown.ts | 6 +-- packages/autocomplete-core/propGetters.ts | 4 +- packages/autocomplete-core/stateReducer.ts | 16 ++++---- packages/autocomplete-core/types/api.ts | 6 +-- packages/autocomplete-core/types/state.ts | 2 +- packages/autocomplete-core/utils.ts | 45 ++++++++++++---------- stories/react.stories.tsx | 4 +- 9 files changed, 49 insertions(+), 44 deletions(-) diff --git a/packages/autocomplete-core/completion.ts b/packages/autocomplete-core/completion.ts index 3173d085e..cda7b95f3 100644 --- a/packages/autocomplete-core/completion.ts +++ b/packages/autocomplete-core/completion.ts @@ -11,9 +11,9 @@ export function getCompletion({ props, }: GetCompletionProps): string | null { if ( - !props.showCompletion || - state.highlightedIndex < 0 || - !state.isOpen || + props.showCompletion === false || + state.isOpen === false || + state.highlightedIndex === null || state.status === 'stalled' ) { return null; diff --git a/packages/autocomplete-core/defaultProps.ts b/packages/autocomplete-core/defaultProps.ts index df700b061..2dbb2cd62 100644 --- a/packages/autocomplete-core/defaultProps.ts +++ b/packages/autocomplete-core/defaultProps.ts @@ -18,7 +18,7 @@ export function getDefaultProps( minLength: 1, placeholder: '', autoFocus: false, - defaultHighlightedIndex: 0, + defaultHighlightedIndex: null, showCompletion: false, stallThreshold: 300, environment, @@ -31,7 +31,7 @@ export function getDefaultProps( id: props.id ?? generateAutocompleteId(), // The following props need to be deeply defaulted. initialState: { - highlightedIndex: 0, + highlightedIndex: null, query: '', suggestions: [], isOpen: false, diff --git a/packages/autocomplete-core/onKeyDown.ts b/packages/autocomplete-core/onKeyDown.ts index fd12f153c..441c7fe10 100644 --- a/packages/autocomplete-core/onKeyDown.ts +++ b/packages/autocomplete-core/onKeyDown.ts @@ -48,7 +48,7 @@ export function onKeyDown({ ); nodeItem?.scrollIntoView(false); - if (store.getState().highlightedIndex >= 0) { + if (store.getState().highlightedIndex !== null) { const { item, itemValue, itemUrl, source } = getHighlightedItem({ state: store.getState(), }); @@ -76,7 +76,7 @@ export function onKeyDown({ (event.target as HTMLInputElement).selectionStart === store.getState().query.length)) && props.showCompletion && - store.getState().highlightedIndex >= 0 + store.getState().highlightedIndex !== null ) { event.preventDefault(); @@ -117,7 +117,7 @@ export function onKeyDown({ } 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) { + if (store.getState().highlightedIndex === null) { return; } diff --git a/packages/autocomplete-core/propGetters.ts b/packages/autocomplete-core/propGetters.ts index 10374c636..51183782c 100644 --- a/packages/autocomplete-core/propGetters.ts +++ b/packages/autocomplete-core/propGetters.ts @@ -127,7 +127,7 @@ export function getPropGetters({ return { 'aria-autocomplete': props.showCompletion ? 'both' : 'list', 'aria-activedescendant': - store.getState().isOpen && store.getState().highlightedIndex >= 0 + store.getState().isOpen && store.getState().highlightedIndex !== null ? `${props.id}-item-${store.getState().highlightedIndex}` : null, 'aria-controls': store.getState().isOpen ? `${props.id}-menu` : null, @@ -228,7 +228,7 @@ export function getPropGetters({ ); props.onStateChange({ state: store.getState() }); - if (store.getState().highlightedIndex >= 0) { + if (store.getState().highlightedIndex !== null) { const { item, itemValue, itemUrl, source } = getHighlightedItem({ state: store.getState(), }); diff --git a/packages/autocomplete-core/stateReducer.ts b/packages/autocomplete-core/stateReducer.ts index d380e105b..7bfe52b0d 100644 --- a/packages/autocomplete-core/stateReducer.ts +++ b/packages/autocomplete-core/stateReducer.ts @@ -81,9 +81,10 @@ export const stateReducer = ( return { ...state, highlightedIndex: getNextHighlightedIndex( - action.value.shiftKey ? 5 : 1, + 1, state.highlightedIndex, - getItemsCount(state) + getItemsCount(state), + props.defaultHighlightedIndex ), }; } @@ -92,9 +93,10 @@ export const stateReducer = ( return { ...state, highlightedIndex: getNextHighlightedIndex( - action.value.shiftKey ? -5 : -1, + -1, state.highlightedIndex, - getItemsCount(state) + getItemsCount(state), + props.defaultHighlightedIndex ), }; } @@ -119,7 +121,7 @@ export const stateReducer = ( case 'submit': { return { ...state, - highlightedIndex: -1, + highlightedIndex: null, isOpen: false, status: 'idle', statusContext: {}, @@ -129,7 +131,7 @@ export const stateReducer = ( case 'reset': { return { ...state, - highlightedIndex: -1, + highlightedIndex: null, isOpen: false, status: 'idle', statusContext: {}, @@ -151,7 +153,7 @@ export const stateReducer = ( return { ...state, isOpen: __DEV__ ? state.isOpen : false, - highlightedIndex: -1, + highlightedIndex: null, }; } diff --git a/packages/autocomplete-core/types/api.ts b/packages/autocomplete-core/types/api.ts index cce9ed114..6b627d44d 100644 --- a/packages/autocomplete-core/types/api.ts +++ b/packages/autocomplete-core/types/api.ts @@ -155,9 +155,9 @@ export interface PublicAutocompleteOptions { /** * The default item index to pre-select. * - * @default 0 + * @default null */ - defaultHighlightedIndex?: number; + defaultHighlightedIndex?: number | null; /** * Whether to show the highlighted suggestion as completion in the input. * @@ -217,7 +217,7 @@ export interface AutocompleteOptions { onStateChange(props: { state: AutocompleteState }): void; placeholder: string; autoFocus: boolean; - defaultHighlightedIndex: number; + defaultHighlightedIndex: number | null; showCompletion: boolean; minLength: number; stallThreshold: number; diff --git a/packages/autocomplete-core/types/state.ts b/packages/autocomplete-core/types/state.ts index 43270678e..12bed6e58 100644 --- a/packages/autocomplete-core/types/state.ts +++ b/packages/autocomplete-core/types/state.ts @@ -1,7 +1,7 @@ import { AutocompleteSuggestion } from './api'; export interface AutocompleteState { - highlightedIndex: number; + highlightedIndex: number | null; query: string; suggestions: Array>; isOpen: boolean; diff --git a/packages/autocomplete-core/utils.ts b/packages/autocomplete-core/utils.ts index 4ff626a23..2a4710af3 100644 --- a/packages/autocomplete-core/utils.ts +++ b/packages/autocomplete-core/utils.ts @@ -5,6 +5,7 @@ import { AutocompleteSource, GetSources, AutocompleteSuggestion, + AutocompleteOptions, } from './types'; export const noop = () => {}; @@ -70,30 +71,34 @@ export function normalizeGetSources( }; } -export function getNextHighlightedIndex( +export function getNextHighlightedIndex( moveAmount: number, - baseIndex: number, - itemCount: number -) { - const itemsLastIndex = itemCount - 1; - - if ( - typeof baseIndex !== 'number' || - baseIndex < 0 || - baseIndex >= itemCount - ) { - baseIndex = moveAmount > 0 ? -1 : itemsLastIndex + 1; + baseIndex: number | null, + itemCount: number, + defaultHighlightedIndex: AutocompleteOptions['defaultHighlightedIndex'] +): number | null { + // We allow circular keyboard navigation from the base index. + // The base index can either be `null` (nothing is highlighted) or `0` + // (the first item is highlighted). + // The base index is allowed to get assigned `null` only if + // `props.defaultHighlightedIndex` is `null`. This pattern allows to "stop" + // by the actual query before navigating to other suggestions as seen on + // Google or Amazon. + if (baseIndex === null && moveAmount < 0) { + return itemCount - 1; } - let newIndex = baseIndex + moveAmount; + if (defaultHighlightedIndex !== null && baseIndex === 0 && moveAmount < 0) { + return itemCount - 1; + } + + const numericIndex = (baseIndex === null ? -1 : baseIndex) + moveAmount; - if (newIndex < 0) { - newIndex = itemsLastIndex; - } else if (newIndex > itemsLastIndex) { - newIndex = 0; + if (numericIndex <= -1 || numericIndex >= itemCount) { + return defaultHighlightedIndex === null ? null : 0; } - return newIndex; + return numericIndex; } // We don't have access to the autocomplete source when we call `onKeyDown` @@ -120,7 +125,7 @@ function getSuggestionFromHighlightedIndex({ // Based on the accumulated counts, we can infer the index of the suggestion. const suggestionIndex = accumulatedSuggestionsCount.reduce((acc, current) => { - if (current <= state.highlightedIndex) { + if (current <= state.highlightedIndex!) { return acc + 1; } @@ -164,7 +169,7 @@ function getRelativeHighlightedIndex({ counter++; } - return state.highlightedIndex - previousItemsOffset; + return state.highlightedIndex! - previousItemsOffset; } export function getHighlightedItem({ diff --git a/stories/react.stories.tsx b/stories/react.stories.tsx index 3b4516aa4..e20a24c69 100644 --- a/stories/react.stories.tsx +++ b/stories/react.stories.tsx @@ -21,8 +21,8 @@ storiesOf('React', module) { return [ { @@ -60,7 +60,6 @@ storiesOf('React', module) { return [ @@ -101,7 +100,6 @@ storiesOf('React', module) render( { return [