From ae5bc366c27bf6c6b788808a72fd0144a7215cda Mon Sep 17 00:00:00 2001 From: Johannes Schill Date: Mon, 10 Dec 2018 14:45:49 +0100 Subject: [PATCH 1/5] Start adding keyboard navigation to VizPicker --- .../dashboard/dashgrid/VizTypePicker.tsx | 76 ++++++++++++++----- .../dashgrid/VizTypePickerPlugin.tsx | 34 +++++++++ public/sass/components/_panel_editor.scss | 13 +++- 3 files changed, 105 insertions(+), 18 deletions(-) create mode 100644 public/app/features/dashboard/dashgrid/VizTypePickerPlugin.tsx diff --git a/public/app/features/dashboard/dashgrid/VizTypePicker.tsx b/public/app/features/dashboard/dashgrid/VizTypePicker.tsx index fc5e19a9d5c..b185b9c7410 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,56 @@ export class VizTypePicker extends PureComponent { this.state = { searchQuery: '', + selected: 0, }; } + get filteredPluginListCount() { + const filteredPluginList = this.getFilteredPluginList(); + return filteredPluginList.length; + } + + goRight = () => { + const maxArray = this.filteredPluginListCount - 1; + const nextIndex = this.state.selected >= maxArray ? 0 : this.state.selected + 1; + this.setState({ + selected: nextIndex, + }); + }; + + goLeft = () => { + const maxArray = this.filteredPluginListCount - 1; + const nextIndex = this.state.selected <= 0 ? maxArray : this.state.selected - 1; + this.setState({ + selected: nextIndex, + }); + }; + + onKeydown = (evt: KeyboardEvent) => { + if (evt.key === 'ArrowRight' || evt.key === 'ArrowDown') { + this.goRight(); + } + if (evt.key === 'ArrowLeft' || evt.key === 'ArrowUp') { + this.goLeft(); + } + if (evt.key === 'Enter') { + const filteredPluginList = this.getFilteredPluginList(); + this.props.onTypeChanged(filteredPluginList[this.state.selected]); + } + }; + + componentDidMount() { + setTimeout(() => { + this.searchInput.focus(); + }, 300); + + document.addEventListener('keydown', this.onKeydown); + } + + componentWillUnmount() { + document.removeEventListener('keydown', this.onKeydown); + } + getPanelPlugins(filter): PanelPlugin[] { const panels = _.chain(config.panels) .filter({ hideFromList: false }) @@ -37,25 +85,19 @@ export class VizTypePicker extends PureComponent { } renderVizPlugin = (plugin: PanelPlugin, index: number) => { - const cssClass = classNames({ - 'viz-picker__item': true, - 'viz-picker__item--selected': plugin.id === this.props.current.id, - }); - + const isSelected = this.state.selected === index; + const isCurrent = plugin.id === this.props.current.id; return ( -
this.props.onTypeChanged(plugin)} title={plugin.name}> -
{plugin.name}
- -
+ this.props.onTypeChanged(plugin)} + /> ); }; - componentDidMount() { - setTimeout(() => { - this.searchInput.focus(); - }, 300); - } - getFilteredPluginList = (): PanelPlugin[] => { const { searchQuery } = this.state; const regex = new RegExp(searchQuery, 'i'); @@ -73,6 +115,7 @@ export class VizTypePicker extends PureComponent { this.setState(prevState => ({ ...prevState, searchQuery: value, + selected: 0, })); }; @@ -102,7 +145,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..534a0f3a756 --- /dev/null +++ b/public/app/features/dashboard/dashgrid/VizTypePickerPlugin.tsx @@ -0,0 +1,34 @@ +import React from 'react'; +import classNames from 'classnames'; + +interface Props { + isSelected: boolean; + isCurrent: boolean; + plugin: any; + onClick: () => void; +} + +const VizTypePickerPlugin = React.memo( + ({ isSelected, isCurrent, plugin, onClick }: 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/sass/components/_panel_editor.scss b/public/sass/components/_panel_editor.scss index f2286991a9c..cd271133fc5 100644 --- a/public/sass/components/_panel_editor.scss +++ b/public/sass/components/_panel_editor.scss @@ -163,7 +163,7 @@ border: $panel-editor-viz-item-border-hover; } - &--selected { + &--current { box-shadow: 0 0 6px $orange; border: 1px solid $orange; @@ -173,6 +173,17 @@ background: $panel-editor-viz-item-bg-hover-active; } } + + &--selected { + box-shadow: 0 0 6px $purple; + border: 1px solid $purple; + + &:hover { + box-shadow: 0 0 6px $purple; + border: 1px solid $purple; + background: $panel-editor-viz-item-bg-hover-active; + } + } } .viz-picker__item-name { From 856c0ee0526b89cd52629482b50e61702f8e8832 Mon Sep 17 00:00:00 2001 From: Johannes Schill Date: Mon, 10 Dec 2018 15:19:14 +0100 Subject: [PATCH 2/5] Fix styling for vizPicker keyboard nav and change so only arrow up/down is OK to use --- .../dashboard/dashgrid/VizTypePicker.tsx | 15 ++++++++++-- .../dashgrid/VizTypePickerPlugin.tsx | 8 ++++--- public/sass/components/_panel_editor.scss | 23 +++---------------- 3 files changed, 21 insertions(+), 25 deletions(-) diff --git a/public/app/features/dashboard/dashgrid/VizTypePicker.tsx b/public/app/features/dashboard/dashgrid/VizTypePicker.tsx index b185b9c7410..abc810d2cd1 100644 --- a/public/app/features/dashboard/dashgrid/VizTypePicker.tsx +++ b/public/app/features/dashboard/dashgrid/VizTypePicker.tsx @@ -50,10 +50,12 @@ export class VizTypePicker extends PureComponent { }; onKeydown = (evt: KeyboardEvent) => { - if (evt.key === 'ArrowRight' || evt.key === 'ArrowDown') { + if (evt.key === 'ArrowDown') { + evt.preventDefault(); this.goRight(); } - if (evt.key === 'ArrowLeft' || evt.key === 'ArrowUp') { + if (evt.key === 'ArrowUp') { + evt.preventDefault(); this.goLeft(); } if (evt.key === 'Enter') { @@ -84,6 +86,12 @@ export class VizTypePicker extends PureComponent { return _.sortBy(panels, 'sort'); } + onMouseEnter = (mouseEnterIndex: number) => { + this.setState({ + selected: mouseEnterIndex, + }); + }; + renderVizPlugin = (plugin: PanelPlugin, index: number) => { const isSelected = this.state.selected === index; const isCurrent = plugin.id === this.props.current.id; @@ -93,6 +101,9 @@ export class VizTypePicker extends PureComponent { isSelected={isSelected} isCurrent={isCurrent} plugin={plugin} + onMouseEnter={() => { + this.onMouseEnter(index); + }} onClick={() => this.props.onTypeChanged(plugin)} /> ); diff --git a/public/app/features/dashboard/dashgrid/VizTypePickerPlugin.tsx b/public/app/features/dashboard/dashgrid/VizTypePickerPlugin.tsx index 534a0f3a756..d4ed96d1434 100644 --- a/public/app/features/dashboard/dashgrid/VizTypePickerPlugin.tsx +++ b/public/app/features/dashboard/dashgrid/VizTypePickerPlugin.tsx @@ -1,15 +1,17 @@ import React from 'react'; import classNames from 'classnames'; +import { PanelPlugin } from 'app/types/plugins'; interface Props { isSelected: boolean; isCurrent: boolean; - plugin: any; + plugin: PanelPlugin; onClick: () => void; + onMouseEnter: () => void; } const VizTypePickerPlugin = React.memo( - ({ isSelected, isCurrent, plugin, onClick }: Props) => { + ({ isSelected, isCurrent, plugin, onClick, onMouseEnter }: Props) => { const cssClass = classNames({ 'viz-picker__item': true, 'viz-picker__item--selected': isSelected, @@ -17,7 +19,7 @@ const VizTypePickerPlugin = React.memo( }); return ( -
+
{plugin.name}
diff --git a/public/sass/components/_panel_editor.scss b/public/sass/components/_panel_editor.scss index cd271133fc5..4824b3dd238 100644 --- a/public/sass/components/_panel_editor.scss +++ b/public/sass/components/_panel_editor.scss @@ -157,32 +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; - - &:hover { - box-shadow: 0 0 6px $orange; - border: 1px solid $orange; - background: $panel-editor-viz-item-bg-hover-active; - } } &--selected { - box-shadow: 0 0 6px $purple; - border: 1px solid $purple; - - &:hover { - box-shadow: 0 0 6px $purple; - border: 1px solid $purple; - 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; } } From cdcc3163107e4a2fe1bd0b0727d0bfaf944b6972 Mon Sep 17 00:00:00 2001 From: Johannes Schill Date: Mon, 10 Dec 2018 15:27:22 +0100 Subject: [PATCH 3/5] Variable rename. Did not make sense at all. --- .../app/features/dashboard/dashgrid/VizTypePicker.tsx | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/public/app/features/dashboard/dashgrid/VizTypePicker.tsx b/public/app/features/dashboard/dashgrid/VizTypePicker.tsx index abc810d2cd1..bc1643abbd7 100644 --- a/public/app/features/dashboard/dashgrid/VizTypePicker.tsx +++ b/public/app/features/dashboard/dashgrid/VizTypePicker.tsx @@ -28,22 +28,20 @@ export class VizTypePicker extends PureComponent { }; } - get filteredPluginListCount() { + get maxSelectedIndex() { const filteredPluginList = this.getFilteredPluginList(); - return filteredPluginList.length; + return filteredPluginList.length - 1; } goRight = () => { - const maxArray = this.filteredPluginListCount - 1; - const nextIndex = this.state.selected >= maxArray ? 0 : this.state.selected + 1; + const nextIndex = this.state.selected >= this.maxSelectedIndex ? 0 : this.state.selected + 1; this.setState({ selected: nextIndex, }); }; goLeft = () => { - const maxArray = this.filteredPluginListCount - 1; - const nextIndex = this.state.selected <= 0 ? maxArray : this.state.selected - 1; + const nextIndex = this.state.selected <= 0 ? this.maxSelectedIndex : this.state.selected - 1; this.setState({ selected: nextIndex, }); From 1ffac5a33dc8327d1588796411072b0ef0e98668 Mon Sep 17 00:00:00 2001 From: Johannes Schill Date: Mon, 10 Dec 2018 21:38:07 +0100 Subject: [PATCH 4/5] Use react's onKeyDown event on the input instead of event listener on document --- public/app/features/dashboard/dashgrid/VizTypePicker.tsx | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/public/app/features/dashboard/dashgrid/VizTypePicker.tsx b/public/app/features/dashboard/dashgrid/VizTypePicker.tsx index bc1643abbd7..eff51ada020 100644 --- a/public/app/features/dashboard/dashgrid/VizTypePicker.tsx +++ b/public/app/features/dashboard/dashgrid/VizTypePicker.tsx @@ -47,7 +47,7 @@ export class VizTypePicker extends PureComponent { }); }; - onKeydown = (evt: KeyboardEvent) => { + onKeyDown = evt => { if (evt.key === 'ArrowDown') { evt.preventDefault(); this.goRight(); @@ -66,12 +66,6 @@ export class VizTypePicker extends PureComponent { setTimeout(() => { this.searchInput.focus(); }, 300); - - document.addEventListener('keydown', this.onKeydown); - } - - componentWillUnmount() { - document.removeEventListener('keydown', this.onKeydown); } getPanelPlugins(filter): PanelPlugin[] { @@ -138,6 +132,7 @@ export class VizTypePicker extends PureComponent { placeholder="" ref={elem => (this.searchInput = elem)} onChange={this.onSearchQueryChange} + onKeyDown={this.onKeyDown} /> From 20134c902b007b37ad96feb0b7a928a681eb0327 Mon Sep 17 00:00:00 2001 From: Johannes Schill Date: Mon, 10 Dec 2018 21:42:53 +0100 Subject: [PATCH 5/5] Add keyboard navigation to datasource picker via a hoc. --- .../dashboard/dashgrid/DataSourcePicker.tsx | 179 ++++++++++-------- .../dashgrid/withKeyboardNavigation.tsx | 65 +++++++ public/sass/components/_panel_editor.scss | 4 +- 3 files changed, 170 insertions(+), 78 deletions(-) create mode 100644 public/app/features/dashboard/dashgrid/withKeyboardNavigation.tsx 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/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 4824b3dd238..ae480c5a3d3 100644 --- a/public/sass/components/_panel_editor.scss +++ b/public/sass/components/_panel_editor.scss @@ -257,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;