From 645812f6435d96728074c93a827406e12affa907 Mon Sep 17 00:00:00 2001 From: Johannes Schill Date: Tue, 11 Dec 2018 09:57:58 +0100 Subject: [PATCH 1/3] Let VizTypePicker use the keyboard navigation hoc --- .../dashboard/dashgrid/DataSourcePicker.tsx | 9 +- .../dashboard/dashgrid/VizTypePicker.tsx | 244 ++++++++---------- .../dashgrid/withKeyboardNavigation.tsx | 11 +- 3 files changed, 122 insertions(+), 142 deletions(-) diff --git a/public/app/features/dashboard/dashgrid/DataSourcePicker.tsx b/public/app/features/dashboard/dashgrid/DataSourcePicker.tsx index 2c33474ee73..5c343d31353 100644 --- a/public/app/features/dashboard/dashgrid/DataSourcePicker.tsx +++ b/public/app/features/dashboard/dashgrid/DataSourcePicker.tsx @@ -1,15 +1,12 @@ import React, { PureComponent } from 'react'; import classNames from 'classnames'; import _ from 'lodash'; -import withKeyboardNavigation from './withKeyboardNavigation'; +import withKeyboardNavigation, { KeyboardNavigationProps } from './withKeyboardNavigation'; import { DataSourceSelectItem } from 'app/types'; export interface Props { - onChangeDataSource: (ds: any) => void; + onChangeDataSource: (ds: DataSourceSelectItem) => void; datasources: DataSourceSelectItem[]; - selected?: number; - onKeyDown?: (evt: any, maxSelectedIndex: number, onEnterAction: () => void) => void; - onMouseEnter?: (select: number) => void; } interface State { @@ -17,7 +14,7 @@ interface State { } export const DataSourcePicker = withKeyboardNavigation( - class DataSourcePicker extends PureComponent { + class DataSourcePicker extends PureComponent { searchInput: HTMLElement; constructor(props) { diff --git a/public/app/features/dashboard/dashgrid/VizTypePicker.tsx b/public/app/features/dashboard/dashgrid/VizTypePicker.tsx index eff51ada020..94c0ef79ad0 100644 --- a/public/app/features/dashboard/dashgrid/VizTypePicker.tsx +++ b/public/app/features/dashboard/dashgrid/VizTypePicker.tsx @@ -4,153 +4,129 @@ import _ from 'lodash'; import config from 'app/core/config'; import { PanelPlugin } from 'app/types/plugins'; import VizTypePickerPlugin from './VizTypePickerPlugin'; +import withKeyboardNavigation, { KeyboardNavigationProps } from './withKeyboardNavigation'; -interface Props { +export interface Props { current: PanelPlugin; onTypeChanged: (newType: PanelPlugin) => void; } interface State { searchQuery: string; - selected: number; } -export class VizTypePicker extends PureComponent { - searchInput: HTMLElement; - pluginList = this.getPanelPlugins(''); +export const VizTypePicker = withKeyboardNavigation( + class VizTypePicker extends PureComponent { + searchInput: HTMLElement; + pluginList = this.getPanelPlugins(''); - constructor(props) { - super(props); + constructor(props) { + super(props); - 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(); + this.state = { + searchQuery: '', + }; } - if (evt.key === 'ArrowUp') { - evt.preventDefault(); - this.goLeft(); - } - if (evt.key === 'Enter') { + + get maxSelectedIndex() { const filteredPluginList = this.getFilteredPluginList(); - this.props.onTypeChanged(filteredPluginList[this.state.selected]); + return filteredPluginList.length - 1; } - }; - componentDidMount() { - setTimeout(() => { - this.searchInput.focus(); - }, 300); + componentDidMount() { + setTimeout(() => { + this.searchInput.focus(); + }, 300); + } + + getPanelPlugins(filter): PanelPlugin[] { + const panels = _.chain(config.panels) + .filter({ hideFromList: false }) + .map(item => item) + .value(); + + // add sort by sort property + return _.sortBy(panels, 'sort'); + } + + renderVizPlugin = (plugin: PanelPlugin, index: number) => { + const { onTypeChanged, selected, onMouseEnter } = this.props; + const isSelected = selected === index; + const isCurrent = plugin.id === this.props.current.id; + return ( + { + onMouseEnter(index); + }} + onClick={() => onTypeChanged(plugin)} + /> + ); + }; + + getFilteredPluginList = (): PanelPlugin[] => { + const { searchQuery } = this.state; + const regex = new RegExp(searchQuery, 'i'); + const pluginList = this.pluginList; + + const filtered = pluginList.filter(item => { + return regex.test(item.name); + }); + + return filtered; + }; + + onSearchQueryChange = evt => { + const value = evt.target.value; + this.setState(prevState => ({ + ...prevState, + searchQuery: value, + })); + }; + + renderFilters = () => { + const { searchQuery } = this.state; + const { onKeyDown } = this.props; + return ( + <> + + + ); + }; + + render() { + const filteredPluginList = this.getFilteredPluginList(); + + return ( + <> +
+ {this.renderFilters()} +
+
+
{filteredPluginList.map(this.renderVizPlugin)}
+ + ); + } } - - getPanelPlugins(filter): PanelPlugin[] { - const panels = _.chain(config.panels) - .filter({ hideFromList: false }) - .map(item => item) - .value(); - - // add sort by sort property - 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; - return ( - { - this.onMouseEnter(index); - }} - onClick={() => this.props.onTypeChanged(plugin)} - /> - ); - }; - - getFilteredPluginList = (): PanelPlugin[] => { - const { searchQuery } = this.state; - const regex = new RegExp(searchQuery, 'i'); - const pluginList = this.pluginList; - - const filtered = pluginList.filter(item => { - return regex.test(item.name); - }); - - return filtered; - }; - - onSearchQueryChange = evt => { - const value = evt.target.value; - this.setState(prevState => ({ - ...prevState, - searchQuery: value, - selected: 0, - })); - }; - - renderFilters = () => { - return ( - <> - - - ); - }; - - render() { - const filteredPluginList = this.getFilteredPluginList(); - - return ( - <> -
- {this.renderFilters()} -
-
-
{filteredPluginList.map(this.renderVizPlugin)}
- - ); - } -} +); diff --git a/public/app/features/dashboard/dashgrid/withKeyboardNavigation.tsx b/public/app/features/dashboard/dashgrid/withKeyboardNavigation.tsx index 58affdf0471..ef20fe96a29 100644 --- a/public/app/features/dashboard/dashgrid/withKeyboardNavigation.tsx +++ b/public/app/features/dashboard/dashgrid/withKeyboardNavigation.tsx @@ -1,12 +1,19 @@ import React from 'react'; -import { Props } from './DataSourcePicker'; +import { Props as DataSourceProps } from './DataSourcePicker'; +import { Props as VizTypeProps } from './VizTypePicker'; interface State { selected: number; } +export interface KeyboardNavigationProps { + selected?: number; + onKeyDown?: (evt: React.KeyboardEvent, maxSelectedIndex: number, onEnterAction: () => void) => void; + onMouseEnter?: (select: number) => void; +} + const withKeyboardNavigation = WrappedComponent => { - return class extends React.Component { + return class extends React.Component { constructor(props) { super(props); From 07ce88f685c113ec134ca3c8eca095f921cc7dc5 Mon Sep 17 00:00:00 2001 From: Johannes Schill Date: Tue, 11 Dec 2018 11:46:59 +0100 Subject: [PATCH 2/3] Clean up hoc and extend component props automatically --- .../dashgrid/withKeyboardNavigation.tsx | 21 +++++++------------ 1 file changed, 7 insertions(+), 14 deletions(-) diff --git a/public/app/features/dashboard/dashgrid/withKeyboardNavigation.tsx b/public/app/features/dashboard/dashgrid/withKeyboardNavigation.tsx index ef20fe96a29..f0b84b6299b 100644 --- a/public/app/features/dashboard/dashgrid/withKeyboardNavigation.tsx +++ b/public/app/features/dashboard/dashgrid/withKeyboardNavigation.tsx @@ -1,19 +1,17 @@ -import React from 'react'; -import { Props as DataSourceProps } from './DataSourcePicker'; -import { Props as VizTypeProps } from './VizTypePicker'; +import React, { KeyboardEvent, ComponentType, Component } from 'react'; interface State { selected: number; } export interface KeyboardNavigationProps { - selected?: number; - onKeyDown?: (evt: React.KeyboardEvent, maxSelectedIndex: number, onEnterAction: () => void) => void; - onMouseEnter?: (select: number) => void; + onKeyDown: (evt: KeyboardEvent, maxSelectedIndex: number, onEnterAction: () => void) => void; + onMouseEnter: (select: number) => void; + selected: number; } -const withKeyboardNavigation = WrappedComponent => { - return class extends React.Component { +const withKeyboardNavigation =

(WrappedComponent: ComponentType

) => { + return class WithKeyboardNavigation extends Component { constructor(props) { super(props); @@ -58,12 +56,7 @@ const withKeyboardNavigation = WrappedComponent => { render() { return ( - + ); } }; From a0da303f80b6fc624c90fe2be3929de324098663 Mon Sep 17 00:00:00 2001 From: Johannes Schill Date: Tue, 11 Dec 2018 13:46:17 +0100 Subject: [PATCH 3/3] Change KeyboardNavigation from hoc to render prop component --- .../dashboard/dashgrid/DataSourcePicker.tsx | 200 ++++++++------- .../dashboard/dashgrid/KeyboardNavigation.tsx | 71 ++++++ .../dashboard/dashgrid/VizTypePicker.tsx | 227 +++++++++--------- .../dashgrid/withKeyboardNavigation.tsx | 65 ----- 4 files changed, 284 insertions(+), 279 deletions(-) create mode 100644 public/app/features/dashboard/dashgrid/KeyboardNavigation.tsx delete 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 5c343d31353..21eaac3e027 100644 --- a/public/app/features/dashboard/dashgrid/DataSourcePicker.tsx +++ b/public/app/features/dashboard/dashgrid/DataSourcePicker.tsx @@ -1,7 +1,7 @@ import React, { PureComponent } from 'react'; import classNames from 'classnames'; import _ from 'lodash'; -import withKeyboardNavigation, { KeyboardNavigationProps } from './withKeyboardNavigation'; +import KeyboardNavigation, { KeyboardNavigationProps } from './KeyboardNavigation'; import { DataSourceSelectItem } from 'app/types'; export interface Props { @@ -13,109 +13,105 @@ interface State { searchQuery: string; } -export const DataSourcePicker = withKeyboardNavigation( - class DataSourcePicker extends PureComponent { - searchInput: HTMLElement; +export class DataSourcePicker extends PureComponent { + searchInput: HTMLElement; - constructor(props) { - super(props); - this.state = { - searchQuery: '', - }; - } - - 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); - }); - - return filtered; - } - - get maxSelectedIndex() { - const filtered = this.getDataSources(); - return filtered.length - 1; - } - - 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}
-
- ); + constructor(props) { + super(props); + this.state = { + searchQuery: '', }; - - 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)}
- - ); - } } -); + + 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); + }); + + return filtered; + } + + get maxSelectedIndex() { + const filtered = this.getDataSources(); + return filtered.length - 1; + } + + renderDataSource = (ds: DataSourceSelectItem, index: number, keyNavProps: KeyboardNavigationProps) => { + const { onChangeDataSource } = this.props; + const { selected, onMouseEnter } = keyNavProps; + 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}
+
+ ); + }; + + componentDidMount() { + setTimeout(() => { + this.searchInput.focus(); + }, 300); + } + + onSearchQueryChange = evt => { + const value = evt.target.value; + this.setState(prevState => ({ + ...prevState, + searchQuery: value, + })); + }; + + renderFilters({ onKeyDown, selected }: KeyboardNavigationProps) { + const { searchQuery } = this.state; + return ( + + ); + } + + render() { + return ( + ( + <> +
+ {this.renderFilters(keyNavProps)} +
+
+
+ {this.getDataSources().map((ds, index) => this.renderDataSource(ds, index, keyNavProps))} +
+ + )} + /> + ); + } +} export default DataSourcePicker; diff --git a/public/app/features/dashboard/dashgrid/KeyboardNavigation.tsx b/public/app/features/dashboard/dashgrid/KeyboardNavigation.tsx new file mode 100644 index 00000000000..dab8371c925 --- /dev/null +++ b/public/app/features/dashboard/dashgrid/KeyboardNavigation.tsx @@ -0,0 +1,71 @@ +import React, { KeyboardEvent, Component } from 'react'; + +interface State { + selected: number; +} + +export interface KeyboardNavigationProps { + onKeyDown: (evt: KeyboardEvent, maxSelectedIndex: number, onEnterAction: () => void) => void; + onMouseEnter: (select: number) => void; + selected: number; +} + +interface Props { + render: (injectProps: any) => void; +} + +class KeyboardNavigation extends 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() { + const injectProps = { + onKeyDown: this.onKeyDown, + onMouseEnter: this.onMouseEnter, + selected: this.state.selected, + }; + + return <>{this.props.render({ ...injectProps })}; + } +} + +export default KeyboardNavigation; diff --git a/public/app/features/dashboard/dashgrid/VizTypePicker.tsx b/public/app/features/dashboard/dashgrid/VizTypePicker.tsx index 94c0ef79ad0..939fa79c289 100644 --- a/public/app/features/dashboard/dashgrid/VizTypePicker.tsx +++ b/public/app/features/dashboard/dashgrid/VizTypePicker.tsx @@ -4,7 +4,7 @@ import _ from 'lodash'; import config from 'app/core/config'; import { PanelPlugin } from 'app/types/plugins'; import VizTypePickerPlugin from './VizTypePickerPlugin'; -import withKeyboardNavigation, { KeyboardNavigationProps } from './withKeyboardNavigation'; +import KeyboardNavigation, { KeyboardNavigationProps } from './KeyboardNavigation'; export interface Props { current: PanelPlugin; @@ -15,118 +15,121 @@ interface State { searchQuery: string; } -export const VizTypePicker = withKeyboardNavigation( - class VizTypePicker extends PureComponent { - searchInput: HTMLElement; - pluginList = this.getPanelPlugins(''); +export class VizTypePicker extends PureComponent { + searchInput: HTMLElement; + pluginList = this.getPanelPlugins(''); - constructor(props) { - super(props); + constructor(props) { + super(props); - this.state = { - searchQuery: '', - }; - } - - get maxSelectedIndex() { - const filteredPluginList = this.getFilteredPluginList(); - return filteredPluginList.length - 1; - } - - componentDidMount() { - setTimeout(() => { - this.searchInput.focus(); - }, 300); - } - - getPanelPlugins(filter): PanelPlugin[] { - const panels = _.chain(config.panels) - .filter({ hideFromList: false }) - .map(item => item) - .value(); - - // add sort by sort property - return _.sortBy(panels, 'sort'); - } - - renderVizPlugin = (plugin: PanelPlugin, index: number) => { - const { onTypeChanged, selected, onMouseEnter } = this.props; - const isSelected = selected === index; - const isCurrent = plugin.id === this.props.current.id; - return ( - { - onMouseEnter(index); - }} - onClick={() => onTypeChanged(plugin)} - /> - ); + this.state = { + searchQuery: '', }; - - getFilteredPluginList = (): PanelPlugin[] => { - const { searchQuery } = this.state; - const regex = new RegExp(searchQuery, 'i'); - const pluginList = this.pluginList; - - const filtered = pluginList.filter(item => { - return regex.test(item.name); - }); - - return filtered; - }; - - onSearchQueryChange = evt => { - const value = evt.target.value; - this.setState(prevState => ({ - ...prevState, - searchQuery: value, - })); - }; - - renderFilters = () => { - const { searchQuery } = this.state; - const { onKeyDown } = this.props; - return ( - <> - - - ); - }; - - render() { - const filteredPluginList = this.getFilteredPluginList(); - - return ( - <> -
- {this.renderFilters()} -
-
-
{filteredPluginList.map(this.renderVizPlugin)}
- - ); - } } -); + + get maxSelectedIndex() { + const filteredPluginList = this.getFilteredPluginList(); + return filteredPluginList.length - 1; + } + + componentDidMount() { + setTimeout(() => { + this.searchInput.focus(); + }, 300); + } + + getPanelPlugins(filter): PanelPlugin[] { + const panels = _.chain(config.panels) + .filter({ hideFromList: false }) + .map(item => item) + .value(); + + // add sort by sort property + return _.sortBy(panels, 'sort'); + } + + renderVizPlugin = (plugin: PanelPlugin, index: number, keyNavProps: KeyboardNavigationProps) => { + const { onTypeChanged } = this.props; + const { selected, onMouseEnter } = keyNavProps; + const isSelected = selected === index; + const isCurrent = plugin.id === this.props.current.id; + return ( + { + onMouseEnter(index); + }} + onClick={() => onTypeChanged(plugin)} + /> + ); + }; + + getFilteredPluginList = (): PanelPlugin[] => { + const { searchQuery } = this.state; + const regex = new RegExp(searchQuery, 'i'); + const pluginList = this.pluginList; + + const filtered = pluginList.filter(item => { + return regex.test(item.name); + }); + + return filtered; + }; + + onSearchQueryChange = evt => { + const value = evt.target.value; + this.setState(prevState => ({ + ...prevState, + searchQuery: value, + })); + }; + + renderFilters = ({ onKeyDown, selected }: KeyboardNavigationProps) => { + const { searchQuery } = this.state; + return ( + <> + + + ); + }; + + render() { + const filteredPluginList = this.getFilteredPluginList(); + + return ( + ( + <> +
+ {this.renderFilters(keyNavProps)} +
+
+
+ {filteredPluginList.map((plugin, index) => this.renderVizPlugin(plugin, index, keyNavProps))} +
+ + )} + /> + ); + } +} diff --git a/public/app/features/dashboard/dashgrid/withKeyboardNavigation.tsx b/public/app/features/dashboard/dashgrid/withKeyboardNavigation.tsx deleted file mode 100644 index f0b84b6299b..00000000000 --- a/public/app/features/dashboard/dashgrid/withKeyboardNavigation.tsx +++ /dev/null @@ -1,65 +0,0 @@ -import React, { KeyboardEvent, ComponentType, Component } from 'react'; - -interface State { - selected: number; -} - -export interface KeyboardNavigationProps { - onKeyDown: (evt: KeyboardEvent, maxSelectedIndex: number, onEnterAction: () => void) => void; - onMouseEnter: (select: number) => void; - selected: number; -} - -const withKeyboardNavigation =

(WrappedComponent: ComponentType

) => { - return class WithKeyboardNavigation extends 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;