Skip to content

Commit

Permalink
feat(core): introduce new completion system (#354)
Browse files Browse the repository at this point in the history
BREAKING CHANGE
  • Loading branch information
francoischalifour authored Nov 4, 2020
1 parent b8ff178 commit 25099e8
Show file tree
Hide file tree
Showing 12 changed files with 29 additions and 132 deletions.
12 changes: 0 additions & 12 deletions packages/autocomplete-core/src/completionStateEnhancer.ts

This file was deleted.

3 changes: 1 addition & 2 deletions packages/autocomplete-core/src/createAutocomplete.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { completionStateEnhancer } from './completionStateEnhancer';
import { createStore } from './createStore';
import { getAutocompleteSetters } from './getAutocompleteSetters';
import { getDefaultProps } from './getDefaultProps';
Expand All @@ -16,7 +15,7 @@ export function createAutocomplete<
options: AutocompleteOptions<TItem>
): AutocompleteApi<TItem, TEvent, TMouseEvent, TKeyboardEvent> {
const props = getDefaultProps(options);
const store = createStore(stateReducer, props, [completionStateEnhancer]);
const store = createStore(stateReducer, props);

const {
setSelectedItemId,
Expand Down
26 changes: 7 additions & 19 deletions packages/autocomplete-core/src/createStore.ts
Original file line number Diff line number Diff line change
@@ -1,38 +1,26 @@
import {
AutocompleteState,
AutocompleteStore,
InternalAutocompleteOptions,
Reducer,
StateEnhancer,
} from './types';

export function createStore<TItem>(
reducer: Reducer,
props: InternalAutocompleteOptions<TItem>,
stateEnhancers: Array<StateEnhancer<TItem>>
props: InternalAutocompleteOptions<TItem>
): AutocompleteStore<TItem> {
function enhanceState(state: AutocompleteState<TItem>) {
return stateEnhancers.reduce(
(nextState, stateEnhancer) => stateEnhancer(nextState, props),
state
);
}

let state = enhanceState(props.initialState);
let state = props.initialState;

return {
getState() {
return state;
},
send(action, payload) {
const prevState = { ...state };
state = enhanceState(
reducer(state, {
type: action,
props,
payload,
})
);
state = reducer(state, {
type: action,
props,
payload,
});

props.onStateChange({ state, prevState });
},
Expand Down
37 changes: 3 additions & 34 deletions packages/autocomplete-core/src/getCompletion.ts
Original file line number Diff line number Diff line change
@@ -1,49 +1,18 @@
import { InternalAutocompleteOptions, AutocompleteState } from './types';
import { AutocompleteState } from './types';
import { getSelectedItem } from './utils';

interface GetCompletionProps<TItem> {
state: AutocompleteState<TItem>;
props: InternalAutocompleteOptions<TItem>;
}

export function getCompletion<TItem>({
state,
props,
}: GetCompletionProps<TItem>): string | null {
if (
props.enableCompletion === false ||
state.isOpen === false ||
state.selectedItemId === null ||
state.status === 'stalled'
) {
if (state.selectedItemId === null) {
return null;
}

const { itemInputValue } = getSelectedItem({ state })!;

// The completion should appear only if the _first_ characters of the query
// match with the item.
if (
state.query.length > 0 &&
itemInputValue
.toLocaleLowerCase()
.indexOf(state.query.toLocaleLowerCase()) === 0
) {
// If the query typed has a different case than the item, we want
// to show the completion matching the case of the query. This makes both
// strings overlap correctly.
// Example:
// - query: 'Gui'
// - item: 'guitar'
// => completion: 'Guitar'
const completion = state.query + itemInputValue.slice(state.query.length);

if (completion === state.query) {
return null;
}

return completion;
}

return null;
return itemInputValue || null;
}
1 change: 0 additions & 1 deletion packages/autocomplete-core/src/getDefaultProps.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@ export function getDefaultProps<TItem>(
placeholder: '',
autoFocus: false,
defaultSelectedItemId: null,
enableCompletion: false,
stallThreshold: 300,
environment,
shouldDropdownShow: ({ state }) => getItemsCount(state) > 0,
Expand Down
6 changes: 3 additions & 3 deletions packages/autocomplete-core/src/getPropGetters.ts
Original file line number Diff line number Diff line change
Expand Up @@ -159,7 +159,7 @@ export function getPropGetters<TItem, TEvent, TMouseEvent, TKeyboardEvent>({
// because the dropdown should open with the current query.
if (props.openOnFocus || store.getState().query.length > 0) {
onInput({
query: store.getState().query,
query: store.getState().completion || store.getState().query,
event,
store,
props,
Expand All @@ -180,14 +180,14 @@ export function getPropGetters<TItem, TEvent, TMouseEvent, TKeyboardEvent>({
const { inputElement, maxLength = 512, ...rest } = providedProps || {};

return {
'aria-autocomplete': props.enableCompletion ? 'both' : 'list',
'aria-autocomplete': 'both',
'aria-activedescendant':
store.getState().isOpen && store.getState().selectedItemId !== null
? `${props.id}-item-${store.getState().selectedItemId}`
: undefined,
'aria-controls': store.getState().isOpen ? `${props.id}-menu` : undefined,
'aria-labelledby': `${props.id}-label`,
value: store.getState().query,
value: store.getState().completion || store.getState().query,
id: `${props.id}-input`,
autoComplete: 'off',
autoCorrect: 'off',
Expand Down
30 changes: 0 additions & 30 deletions packages/autocomplete-core/src/onKeyDown.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { getCompletion } from './getCompletion';
import { onInput } from './onInput';
import {
InternalAutocompleteOptions,
Expand Down Expand Up @@ -68,35 +67,6 @@ export function onKeyDown<TItem>({
event,
});
}
} else if (
(event.key === 'Tab' ||
// When the user hits the right arrow and is at the end of the input
// query, we validate the completion.
(event.key === 'ArrowRight' &&
(event.target as HTMLInputElement).selectionStart ===
store.getState().query.length)) &&
props.enableCompletion &&
store.getState().selectedItemId !== null
) {
event.preventDefault();

const query = getCompletion({ state: store.getState(), props });

if (query) {
onInput({
query,
event,
store,
props,
setSelectedItemId,
setQuery,
setCollections,
setIsOpen,
setStatus,
setContext,
refresh,
});
}
} else if (event.key === 'Escape') {
// This prevents the default browser behavior on `input[type="search"]`
// to remove the query right away because we first want to close the
Expand Down
17 changes: 15 additions & 2 deletions packages/autocomplete-core/src/stateReducer.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { getCompletion } from './getCompletion';
import { Reducer } from './types';
import { getItemsCount, getNextSelectedItemId } from './utils';

Expand All @@ -14,6 +15,7 @@ export const stateReducer: Reducer = (state, action) => {
return {
...state,
query: action.payload,
completion: null,
};
}

Expand Down Expand Up @@ -49,7 +51,7 @@ export const stateReducer: Reducer = (state, action) => {
}

case 'ArrowDown': {
return {
const nextState = {
...state,
selectedItemId: getNextSelectedItemId(
1,
Expand All @@ -58,10 +60,15 @@ export const stateReducer: Reducer = (state, action) => {
action.props.defaultSelectedItemId
),
};

return {
...nextState,
completion: getCompletion({ state: nextState }),
};
}

case 'ArrowUp': {
return {
const nextState = {
...state,
selectedItemId: getNextSelectedItemId(
-1,
Expand All @@ -70,13 +77,19 @@ export const stateReducer: Reducer = (state, action) => {
action.props.defaultSelectedItemId
),
};

return {
...nextState,
completion: getCompletion({ state: nextState }),
};
}

case 'Escape': {
if (state.isOpen) {
return {
...state,
isOpen: false,
completion: null,
};
}

Expand Down
7 changes: 0 additions & 7 deletions packages/autocomplete-core/src/types/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -196,12 +196,6 @@ export interface AutocompleteOptions<TItem> {
* @default null
*/
defaultSelectedItemId?: number | null;
/**
* Whether to show the highlighted suggestion as completion in the input.
*
* @default false
*/
enableCompletion?: boolean;
/**
* Whether to open the dropdown on focus when there's no query.
*
Expand Down Expand Up @@ -270,7 +264,6 @@ export interface InternalAutocompleteOptions<TItem>
placeholder: string;
autoFocus: boolean;
defaultSelectedItemId: number | null;
enableCompletion: boolean;
openOnFocus: boolean;
stallThreshold: number;
initialState: AutocompleteState<TItem>;
Expand Down
5 changes: 0 additions & 5 deletions packages/autocomplete-core/src/types/store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,3 @@ type ActionType =
| 'mousemove'
| 'mouseleave'
| 'click';

export type StateEnhancer<TItem> = (
state: AutocompleteState<TItem>,
props: InternalAutocompleteOptions<TItem>
) => AutocompleteState<TItem>;
11 changes: 0 additions & 11 deletions packages/autocomplete-js/src/autocomplete.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,6 @@ export function autocomplete<TItem>({
}: AutocompleteOptions<TItem>): AutocompleteApi<TItem> {
const containerElement = getHTMLElement(container);
const inputWrapper = document.createElement('div');
const completion = document.createElement('span');
const input = document.createElement('input');
const root = document.createElement('div');
const form = document.createElement('form');
Expand Down Expand Up @@ -94,9 +93,6 @@ export function autocomplete<TItem>({
...autocomplete.getInputProps({ inputElement: input }),
class: concatClassNames(['aa-Input', classNames.input]),
});
setProperties(completion, {
class: concatClassNames(['aa-Completion', classNames.completion]),
});
setProperties(label, {
...autocomplete.getLabelProps(),
class: concatClassNames(['aa-Label', classNames.label]),
Expand All @@ -121,10 +117,6 @@ export function autocomplete<TItem>({
autocomplete.getInputProps({ inputElement: input })
);

if (props.enableCompletion) {
completion.textContent = state.completion;
}

dropdown.innerHTML = '';

if (state.isOpen) {
Expand Down Expand Up @@ -214,9 +206,6 @@ export function autocomplete<TItem>({
renderDropdown({ root: dropdown, sections, state });
}

if (props.enableCompletion) {
inputWrapper.appendChild(completion);
}
inputWrapper.appendChild(input);
inputWrapper.appendChild(label);
inputWrapper.appendChild(resetButton);
Expand Down
6 changes: 0 additions & 6 deletions packages/website/docs/partials/createAutocomplete-props.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,12 +36,6 @@ The default item index to pre-select.

We recommend using `0` when the query typed aims at opening suggestion links, without triggering an actual search.

### `enableCompletion`

> `boolean` | defaults to `false`
Whether to show the highlighted suggestion as completion in the input.

### `openOnFocus`

> `boolean` | defaults to `false`
Expand Down

0 comments on commit 25099e8

Please sign in to comment.