Skip to content

Commit

Permalink
Editor: No default suggestion selected (#24479)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
davkal committed May 11, 2020
1 parent b16202a commit 34f6193
Show file tree
Hide file tree
Showing 3 changed files with 69 additions and 12 deletions.
46 changes: 46 additions & 0 deletions packages/grafana-ui/src/components/Typeahead/Typeahead.test.tsx
@@ -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();
});
});
34 changes: 23 additions & 11 deletions packages/grafana-ui/src/components/Typeahead/Typeahead.tsx
Expand Up @@ -20,21 +20,28 @@ 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> {
static contextType = ThemeContext;
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) {
Expand Down Expand Up @@ -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;
Expand All @@ -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 });
}
};

Expand All @@ -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);
Expand All @@ -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]);
}
};
Expand Down Expand Up @@ -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}>
Expand All @@ -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}
Expand All @@ -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>
);
}
Expand Down
1 change: 0 additions & 1 deletion public/sass/components/_slate_editor.scss
Expand Up @@ -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;
Expand Down

0 comments on commit 34f6193

Please sign in to comment.