Просмотр исходного кода

Add keyboard navigation to datasource picker via a hoc.

Johannes Schill 7 лет назад
Родитель
Сommit
20134c902b

+ 100 - 73
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<Props, State> {
-  searchInput: HTMLElement;
+export const DataSourcePicker = withKeyboardNavigation(
+  class DataSourcePicker extends PureComponent<Props, State> {
+    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;
+    }
+
+    get maxSelectedIndex() {
+      const filtered = this.getDataSources();
+      return filtered.length - 1;
+    }
 
-  renderDataSource = (ds: DataSourceSelectItem, index: number) => {
-    const { onChangeDataSource } = this.props;
-    const onClick = () => onChangeDataSource(ds);
-    const cssClass = classNames({
-      'ds-picker-list__item': true,
-    });
+    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 (
+        <div
+          key={index}
+          className={cssClass}
+          title={ds.name}
+          onClick={onClick}
+          onMouseEnter={() => onMouseEnter(index)}
+        >
+          <img className="ds-picker-list__img" src={ds.meta.info.logos.small} />
+          <div className="ds-picker-list__name">{ds.name}</div>
+        </div>
+      );
+    };
 
-    return (
-      <div key={index} className={cssClass} title={ds.name} onClick={onClick}>
-        <img className="ds-picker-list__img" src={ds.meta.info.logos.small} />
-        <div className="ds-picker-list__name">{ds.name}</div>
-      </div>
-    );
-  };
+    componentDidMount() {
+      setTimeout(() => {
+        this.searchInput.focus();
+      }, 300);
+    }
 
-  componentDidMount() {
-    setTimeout(() => {
-      this.searchInput.focus();
-    }, 300);
-  }
+    onSearchQueryChange = evt => {
+      const value = evt.target.value;
+      this.setState(prevState => ({
+        ...prevState,
+        searchQuery: value,
+      }));
+    };
 
-  onSearchQueryChange = evt => {
-    const value = evt.target.value;
-    this.setState(prevState => ({
-      ...prevState,
-      searchQuery: value,
-    }));
-  };
+    renderFilters() {
+      const { searchQuery } = this.state;
+      const { onKeyDown } = this.props;
+      return (
+        <>
+          <label className="gf-form--has-input-icon">
+            <input
+              type="text"
+              className="gf-form-input width-13"
+              placeholder=""
+              ref={elem => (this.searchInput = elem)}
+              onChange={this.onSearchQueryChange}
+              value={searchQuery}
+              onKeyDown={evt => {
+                onKeyDown(evt, this.maxSelectedIndex, () => {
+                  const { onChangeDataSource, selected } = this.props;
+                  const ds = this.getDataSources()[selected];
+                  onChangeDataSource(ds);
+                });
+              }}
+            />
+            <i className="gf-form-input-icon fa fa-search" />
+          </label>
+        </>
+      );
+    }
 
-  renderFilters() {
-    const { searchQuery } = this.state;
-    return (
-      <>
-        <label className="gf-form--has-input-icon">
-          <input
-            type="text"
-            className="gf-form-input width-13"
-            placeholder=""
-            ref={elem => (this.searchInput = elem)}
-            onChange={this.onSearchQueryChange}
-            value={searchQuery}
-          />
-          <i className="gf-form-input-icon fa fa-search" />
-        </label>
-      </>
-    );
+    render() {
+      return (
+        <>
+          <div className="cta-form__bar">
+            {this.renderFilters()}
+            <div className="gf-form--grow" />
+          </div>
+          <div className="ds-picker-list">{this.getDataSources().map(this.renderDataSource)}</div>
+        </>
+      );
+    }
   }
+);
 
-  render() {
-    return (
-      <>
-        <div className="cta-form__bar">
-          {this.renderFilters()}
-          <div className="gf-form--grow" />
-        </div>
-        <div className="ds-picker-list">{this.getDataSources().map(this.renderDataSource)}</div>
-      </>
-    );
-  }
-}
+export default DataSourcePicker;

+ 65 - 0
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<Props, State> {
+    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 (
+        <WrappedComponent
+          selected={this.state.selected}
+          onKeyDown={this.onKeyDown}
+          onMouseEnter={this.onMouseEnter}
+          {...this.props}
+        />
+      );
+    }
+  };
+};
+
+export default withKeyboardNavigation;

+ 2 - 2
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;