diff --git a/public/app/features/dashboard/dashgrid/DataSourcePicker.tsx b/public/app/features/dashboard/dashgrid/DataSourcePicker.tsx index 9a3923a09f2..2c33474ee73 100644 --- a/public/app/features/dashboard/dashgrid/DataSourcePicker.tsx +++ b/public/app/features/dashboard/dashgrid/DataSourcePicker.tsx @@ -1,97 +1,124 @@ import React, { PureComponent } from 'react'; import classNames from 'classnames'; import _ from 'lodash'; - +import withKeyboardNavigation from './withKeyboardNavigation'; import { DataSourceSelectItem } from 'app/types'; -interface Props { +export interface Props { onChangeDataSource: (ds: any) => void; datasources: DataSourceSelectItem[]; + selected?: number; + onKeyDown?: (evt: any, maxSelectedIndex: number, onEnterAction: () => void) => void; + onMouseEnter?: (select: number) => void; } interface State { searchQuery: string; } -export class DataSourcePicker extends PureComponent { - searchInput: HTMLElement; +export const DataSourcePicker = withKeyboardNavigation( + class DataSourcePicker extends PureComponent { + searchInput: HTMLElement; - constructor(props) { - super(props); - this.state = { - searchQuery: '', - }; - } + constructor(props) { + super(props); + this.state = { + searchQuery: '', + }; + } - getDataSources() { - const { searchQuery } = this.state; - const regex = new RegExp(searchQuery, 'i'); - const { datasources } = this.props; + getDataSources() { + const { searchQuery } = this.state; + const regex = new RegExp(searchQuery, 'i'); + const { datasources } = this.props; - const filtered = datasources.filter(item => { - return regex.test(item.name) || regex.test(item.meta.name); - }); + const filtered = datasources.filter(item => { + return regex.test(item.name) || regex.test(item.meta.name); + }); - return filtered; - } + return filtered; + } - renderDataSource = (ds: DataSourceSelectItem, index: number) => { - const { onChangeDataSource } = this.props; - const onClick = () => onChangeDataSource(ds); - const cssClass = classNames({ - 'ds-picker-list__item': true, - }); + get maxSelectedIndex() { + const filtered = this.getDataSources(); + return filtered.length - 1; + } - return ( -
- -
{ds.name}
-
- ); - }; - - componentDidMount() { - setTimeout(() => { - this.searchInput.focus(); - }, 300); - } - - onSearchQueryChange = evt => { - const value = evt.target.value; - this.setState(prevState => ({ - ...prevState, - searchQuery: value, - })); - }; - - renderFilters() { - const { searchQuery } = this.state; - return ( - <> - - - ); - } - - render() { - return ( - <> -
- {this.renderFilters()} -
+ renderDataSource = (ds: DataSourceSelectItem, index: number) => { + const { onChangeDataSource, selected, onMouseEnter } = this.props; + const onClick = () => onChangeDataSource(ds); + const isSelected = selected === index; + const cssClass = classNames({ + 'ds-picker-list__item': true, + 'ds-picker-list__item--selected': isSelected, + }); + return ( +
onMouseEnter(index)} + > + +
{ds.name}
-
{this.getDataSources().map(this.renderDataSource)}
- - ); + ); + }; + + componentDidMount() { + setTimeout(() => { + this.searchInput.focus(); + }, 300); + } + + onSearchQueryChange = evt => { + const value = evt.target.value; + this.setState(prevState => ({ + ...prevState, + searchQuery: value, + })); + }; + + renderFilters() { + const { searchQuery } = this.state; + const { onKeyDown } = this.props; + return ( + <> + + + ); + } + + render() { + return ( + <> +
+ {this.renderFilters()} +
+
+
{this.getDataSources().map(this.renderDataSource)}
+ + ); + } } -} +); + +export default DataSourcePicker; diff --git a/public/app/features/dashboard/dashgrid/VizTypePicker.tsx b/public/app/features/dashboard/dashgrid/VizTypePicker.tsx index fc5e19a9d5c..eff51ada020 100644 --- a/public/app/features/dashboard/dashgrid/VizTypePicker.tsx +++ b/public/app/features/dashboard/dashgrid/VizTypePicker.tsx @@ -1,9 +1,9 @@ import React, { PureComponent } from 'react'; -import classNames from 'classnames'; import _ from 'lodash'; import config from 'app/core/config'; import { PanelPlugin } from 'app/types/plugins'; +import VizTypePickerPlugin from './VizTypePickerPlugin'; interface Props { current: PanelPlugin; @@ -12,6 +12,7 @@ interface Props { interface State { searchQuery: string; + selected: number; } export class VizTypePicker extends PureComponent { @@ -23,9 +24,50 @@ export class VizTypePicker extends PureComponent { this.state = { searchQuery: '', + selected: 0, }; } + get maxSelectedIndex() { + const filteredPluginList = this.getFilteredPluginList(); + return filteredPluginList.length - 1; + } + + goRight = () => { + const nextIndex = this.state.selected >= this.maxSelectedIndex ? 0 : this.state.selected + 1; + this.setState({ + selected: nextIndex, + }); + }; + + goLeft = () => { + const nextIndex = this.state.selected <= 0 ? this.maxSelectedIndex : this.state.selected - 1; + this.setState({ + selected: nextIndex, + }); + }; + + onKeyDown = evt => { + if (evt.key === 'ArrowDown') { + evt.preventDefault(); + this.goRight(); + } + if (evt.key === 'ArrowUp') { + evt.preventDefault(); + this.goLeft(); + } + if (evt.key === 'Enter') { + const filteredPluginList = this.getFilteredPluginList(); + this.props.onTypeChanged(filteredPluginList[this.state.selected]); + } + }; + + componentDidMount() { + setTimeout(() => { + this.searchInput.focus(); + }, 300); + } + getPanelPlugins(filter): PanelPlugin[] { const panels = _.chain(config.panels) .filter({ hideFromList: false }) @@ -36,25 +78,28 @@ export class VizTypePicker extends PureComponent { return _.sortBy(panels, 'sort'); } - renderVizPlugin = (plugin: PanelPlugin, index: number) => { - const cssClass = classNames({ - 'viz-picker__item': true, - 'viz-picker__item--selected': plugin.id === this.props.current.id, + onMouseEnter = (mouseEnterIndex: number) => { + this.setState({ + selected: mouseEnterIndex, }); - - return ( -
this.props.onTypeChanged(plugin)} title={plugin.name}> -
{plugin.name}
- -
- ); }; - componentDidMount() { - setTimeout(() => { - this.searchInput.focus(); - }, 300); - } + renderVizPlugin = (plugin: PanelPlugin, index: number) => { + const isSelected = this.state.selected === index; + const isCurrent = plugin.id === this.props.current.id; + return ( + { + this.onMouseEnter(index); + }} + onClick={() => this.props.onTypeChanged(plugin)} + /> + ); + }; getFilteredPluginList = (): PanelPlugin[] => { const { searchQuery } = this.state; @@ -73,6 +118,7 @@ export class VizTypePicker extends PureComponent { this.setState(prevState => ({ ...prevState, searchQuery: value, + selected: 0, })); }; @@ -86,6 +132,7 @@ export class VizTypePicker extends PureComponent { placeholder="" ref={elem => (this.searchInput = elem)} onChange={this.onSearchQueryChange} + onKeyDown={this.onKeyDown} /> @@ -102,7 +149,6 @@ export class VizTypePicker extends PureComponent { {this.renderFilters()}
-
{filteredPluginList.map(this.renderVizPlugin)}
); diff --git a/public/app/features/dashboard/dashgrid/VizTypePickerPlugin.tsx b/public/app/features/dashboard/dashgrid/VizTypePickerPlugin.tsx new file mode 100644 index 00000000000..d4ed96d1434 --- /dev/null +++ b/public/app/features/dashboard/dashgrid/VizTypePickerPlugin.tsx @@ -0,0 +1,36 @@ +import React from 'react'; +import classNames from 'classnames'; +import { PanelPlugin } from 'app/types/plugins'; + +interface Props { + isSelected: boolean; + isCurrent: boolean; + plugin: PanelPlugin; + onClick: () => void; + onMouseEnter: () => void; +} + +const VizTypePickerPlugin = React.memo( + ({ isSelected, isCurrent, plugin, onClick, onMouseEnter }: Props) => { + const cssClass = classNames({ + 'viz-picker__item': true, + 'viz-picker__item--selected': isSelected, + 'viz-picker__item--current': isCurrent, + }); + + return ( +
+
{plugin.name}
+ +
+ ); + }, + (prevProps, nextProps) => { + if (prevProps.isSelected === nextProps.isSelected && prevProps.isCurrent === nextProps.isCurrent) { + return true; + } + return false; + } +); + +export default VizTypePickerPlugin; diff --git a/public/app/features/dashboard/dashgrid/withKeyboardNavigation.tsx b/public/app/features/dashboard/dashgrid/withKeyboardNavigation.tsx new file mode 100644 index 00000000000..58affdf0471 --- /dev/null +++ b/public/app/features/dashboard/dashgrid/withKeyboardNavigation.tsx @@ -0,0 +1,65 @@ +import React from 'react'; +import { Props } from './DataSourcePicker'; + +interface State { + selected: number; +} + +const withKeyboardNavigation = WrappedComponent => { + return class extends React.Component { + constructor(props) { + super(props); + + this.state = { + selected: 0, + }; + } + + goToNext = (maxSelectedIndex: number) => { + const nextIndex = this.state.selected >= maxSelectedIndex ? 0 : this.state.selected + 1; + this.setState({ + selected: nextIndex, + }); + }; + + goToPrev = (maxSelectedIndex: number) => { + const nextIndex = this.state.selected <= 0 ? maxSelectedIndex : this.state.selected - 1; + this.setState({ + selected: nextIndex, + }); + }; + + onKeyDown = (evt: KeyboardEvent, maxSelectedIndex: number, onEnterAction: any) => { + if (evt.key === 'ArrowDown') { + evt.preventDefault(); + this.goToNext(maxSelectedIndex); + } + if (evt.key === 'ArrowUp') { + evt.preventDefault(); + this.goToPrev(maxSelectedIndex); + } + if (evt.key === 'Enter' && onEnterAction) { + onEnterAction(); + } + }; + + onMouseEnter = (mouseEnterIndex: number) => { + this.setState({ + selected: mouseEnterIndex, + }); + }; + + render() { + return ( + + ); + } + }; +}; + +export default withKeyboardNavigation; diff --git a/public/sass/components/_panel_editor.scss b/public/sass/components/_panel_editor.scss index f2286991a9c..ae480c5a3d3 100644 --- a/public/sass/components/_panel_editor.scss +++ b/public/sass/components/_panel_editor.scss @@ -157,21 +157,15 @@ padding-bottom: 6px; transition: transform 1 ease; - &:hover { - box-shadow: $panel-editor-viz-item-shadow-hover; - background: $panel-editor-viz-item-bg-hover; - border: $panel-editor-viz-item-border-hover; + &--current { + box-shadow: 0 0 6px $orange; + border: 1px solid $orange; } &--selected { - box-shadow: 0 0 6px $orange; - border: 1px solid $orange; - - &:hover { - box-shadow: 0 0 6px $orange; - border: 1px solid $orange; - background: $panel-editor-viz-item-bg-hover-active; - } + box-shadow: $panel-editor-viz-item-shadow-hover; + background: $panel-editor-viz-item-bg-hover; + border: $panel-editor-viz-item-border-hover; } } @@ -263,13 +257,13 @@ align-items: center; height: 44px; - &:hover { + &--selected { background: $panel-editor-viz-item-bg-hover; border: $panel-editor-viz-item-border-hover; box-shadow: $panel-editor-viz-item-shadow-hover; } - &--selected { + &--active { box-shadow: 0 0 6px $orange; border: 1px solid $orange;