Editor: No default suggestion selected (#24479)

* QueryField: No default suggestion selected

It's been a long-standing issue that careless typing lead to unwanted
tab completion insertions. With this change the completion item list no
longer selects the first item by default. The user has to actively click
ArrowDown to select the first one.

* Added type export

* Remove width limit of typeahead list
This commit is contained in:
David
2020-05-11 14:01:35 +02:00
committed by GitHub
parent b16202acc2
commit 34f61934a1
3 changed files with 69 additions and 12 deletions
@@ -0,0 +1,46 @@
import React from 'react';
import { mount } from 'enzyme';
import { Typeahead, State } from './Typeahead';
import { TypeaheadItem } from './TypeaheadItem';
import { CompletionItemKind } from '../../types';
describe('Typeahead', () => {
const completionItemGroups = [{ label: 'my group', items: [{ label: 'first item' }] }];
describe('when closed', () => {
it('renders nothing when no items given', () => {
const component = mount(<Typeahead origin="test" groupedItems={[]} />);
expect(component.find('.typeahead')).toHaveLength(0);
});
it('renders nothing when items given', () => {
const component = mount(<Typeahead origin="test" groupedItems={completionItemGroups} />);
expect(component.find('.typeahead')).toHaveLength(0);
});
});
describe('when open', () => {
it('renders given items and nothing is selected', () => {
const component = mount(<Typeahead origin="test" groupedItems={completionItemGroups} isOpen />);
expect(component.find('.typeahead')).toHaveLength(1);
const items = component.find(TypeaheadItem);
expect(items).toHaveLength(2);
expect(items.get(0).props.item.kind).toEqual(CompletionItemKind.GroupTitle);
expect(items.get(0).props.isSelected).toBeFalsy();
expect(items.get(1).props.item.label).toEqual('first item');
expect(items.get(1).props.isSelected).toBeFalsy();
});
});
it('selected the first non-group item on moving to first item', () => {
const component = mount(<Typeahead origin="test" groupedItems={completionItemGroups} isOpen />);
expect(component.find('.typeahead')).toHaveLength(1);
let items = component.find(TypeaheadItem);
expect(items).toHaveLength(2);
expect((component.state() as State).typeaheadIndex).toBe(null);
(component.instance() as Typeahead).moveMenuIndex(1);
expect((component.state() as State).typeaheadIndex).toBe(1);
component.setProps({});
items = component.find(TypeaheadItem);
expect(items.get(0).props.isSelected).toBeFalsy();
expect(items.get(1).props.isSelected).toBeTruthy();
});
});
@@ -20,13 +20,13 @@ interface Props {
isOpen?: boolean;
}
interface State {
export interface State {
allItems: CompletionItem[];
listWidth: number;
listHeight: number;
itemHeight: number;
hoveredItem: number | null;
typeaheadIndex: number;
typeaheadIndex: number | null;
}
export class Typeahead extends React.PureComponent<Props, State> {
@@ -34,7 +34,14 @@ export class Typeahead extends React.PureComponent<Props, State> {
context!: React.ContextType<typeof ThemeContext>;
listRef = createRef<FixedSizeList>();
state: State = { hoveredItem: null, typeaheadIndex: 1, allItems: [], listWidth: -1, listHeight: -1, itemHeight: -1 };
state: State = {
hoveredItem: null,
typeaheadIndex: null,
allItems: [],
listWidth: -1,
listHeight: -1,
itemHeight: -1,
};
componentDidMount = () => {
if (this.props.menuRef) {
@@ -63,7 +70,12 @@ export class Typeahead extends React.PureComponent<Props, State> {
};
componentDidUpdate = (prevProps: Readonly<Props>, prevState: Readonly<State>) => {
if (prevState.typeaheadIndex !== this.state.typeaheadIndex && this.listRef && this.listRef.current) {
if (
this.state.typeaheadIndex !== null &&
prevState.typeaheadIndex !== this.state.typeaheadIndex &&
this.listRef &&
this.listRef.current
) {
if (this.state.typeaheadIndex === 1) {
this.listRef.current.scrollToItem(0); // special case for handling the first group label
return;
@@ -75,7 +87,7 @@ export class Typeahead extends React.PureComponent<Props, State> {
const allItems = flattenGroupItems(this.props.groupedItems);
const longestLabel = calculateLongestLabel(allItems);
const { listWidth, listHeight, itemHeight } = calculateListSizes(this.context, allItems, longestLabel);
this.setState({ listWidth, listHeight, itemHeight, allItems });
this.setState({ listWidth, listHeight, itemHeight, allItems, typeaheadIndex: null });
}
};
@@ -95,7 +107,8 @@ export class Typeahead extends React.PureComponent<Props, State> {
const itemCount = this.state.allItems.length;
if (itemCount) {
// Select next suggestion
let newTypeaheadIndex = modulo(this.state.typeaheadIndex + moveAmount, itemCount);
const typeaheadIndex = this.state.typeaheadIndex || 0;
let newTypeaheadIndex = modulo(typeaheadIndex + moveAmount, itemCount);
if (this.state.allItems[newTypeaheadIndex].kind === CompletionItemKind.GroupTitle) {
newTypeaheadIndex = modulo(newTypeaheadIndex + moveAmount, itemCount);
@@ -110,7 +123,7 @@ export class Typeahead extends React.PureComponent<Props, State> {
};
insertSuggestion = () => {
if (this.props.onSelectSuggestion) {
if (this.props.onSelectSuggestion && this.state.typeaheadIndex !== null) {
this.props.onSelectSuggestion(this.state.allItems[this.state.typeaheadIndex]);
}
};
@@ -144,6 +157,7 @@ export class Typeahead extends React.PureComponent<Props, State> {
const { allItems, listWidth, listHeight, itemHeight, hoveredItem, typeaheadIndex } = this.state;
const showDocumentation = hoveredItem || typeaheadIndex;
const documentationItem = allItems[hoveredItem ? hoveredItem : typeaheadIndex || 0];
return (
<Portal origin={origin} isOpen={isOpen} style={this.menuPosition}>
@@ -169,7 +183,7 @@ export class Typeahead extends React.PureComponent<Props, State> {
return (
<TypeaheadItem
onClickItem={() => (this.props.onSelectSuggestion ? this.props.onSelectSuggestion(item) : {})}
isSelected={allItems[typeaheadIndex] === item}
isSelected={typeaheadIndex === null ? false : allItems[typeaheadIndex] === item}
item={item}
prefix={prefix}
style={style}
@@ -181,9 +195,7 @@ export class Typeahead extends React.PureComponent<Props, State> {
</FixedSizeList>
</ul>
{showDocumentation && (
<TypeaheadInfo height={listHeight} item={allItems[hoveredItem ? hoveredItem : typeaheadIndex]} />
)}
{showDocumentation && <TypeaheadInfo height={listHeight} item={documentationItem} />}
</Portal>
);
}
@@ -35,7 +35,6 @@
border: $panel-border;
max-height: calc(66vh);
overflow-y: scroll;
max-width: calc(66%);
overflow-x: hidden;
outline: none;
list-style: none;