Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(core): introduce new completion system #354

Merged
merged 2 commits into from
Nov 4, 2020
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
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 @@ -21,7 +21,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',
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For a textbox with the aria-autocomplete attribute set to either inline or both, authors should ensure that any auto-completed text is selected, so the user can type over it.

https://accessibilityresources.org/aria-autocomplete

I don't see any sites doing this though, so I don't think it really makes sense like that

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

From https://www.digitala11y.com/aria-autocomplete-properties/:

It distinguishes between two models: the inline model (aria-autocomplete=”inline”) that presents a value completion prediction inside the text input and the list model (aria-autocomplete=”list”) that presents a collection of possible values in a separate element that pops up adjacent to the text input. It is possible for an input to offer both models at the same time (aria-autocomplete=”both”).

Since we provide both the completion in the input and suggestions in the dropdown, I believe both is the right value for us.

'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
16 changes: 14 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,6 +77,11 @@ export const stateReducer: Reducer = (state, action) => {
action.props.defaultSelectedItemId
),
};

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

case 'Escape': {
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 @@ -216,12 +216,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 @@ -290,7 +284,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