فهرست منبع

10389 react tooltip components (#10473)

* poc: Use react-popper for tooltips #10389

* poc: Add popover component and use a hoc() for Tooltip + Popover to avoid code duplication #10389

* jest: Add snapshot tests to Popover and Tooltip #10389

* poc: Move target from hoc into Popover/Tooltip-component #10389

* poc: Clean up unused styles and use the existing Grafana style/colors on popper tooltip #10389

* poc: Remove test code before PR

* poc: Remove imports used in poc but shouldn't be included anymore #10389
Johannes Schill 8 سال پیش
والد
کامیت
e4a2bda4f2

+ 1 - 0
package.json

@@ -149,6 +149,7 @@
     "react": "^16.2.0",
     "react-dom": "^16.2.0",
     "react-grid-layout": "^0.16.1",
+    "react-popper": "^0.7.5",
     "react-sizeme": "^2.3.6",
     "remarkable": "^1.7.1",
     "rxjs": "^5.4.3",

+ 11 - 0
public/app/containers/AlertRuleList/AlertRuleList.tsx

@@ -23,6 +23,11 @@ export class AlertRuleList extends React.Component<IContainerProps, any> {
 
     this.props.nav.load('alerting', 'alert-list');
     this.fetchRules();
+    this.handleTooltipPositionChange = this.handleTooltipPositionChange.bind(this);
+
+    this.state = {
+      tooltipPosition: 'auto',
+    };
   }
 
   onStateFilterChanged = evt => {
@@ -44,6 +49,12 @@ export class AlertRuleList extends React.Component<IContainerProps, any> {
     });
   };
 
+  handleTooltipPositionChange(evt) {
+    evt.preventDefault();
+    this.setState({
+      tooltipPosition: evt.target.value,
+    });
+  }
   render() {
     const { nav, alertList } = this.props;
 

+ 16 - 0
public/app/core/components/Tooltip/Popover.jest.tsx

@@ -0,0 +1,16 @@
+import React from 'react';
+import renderer from 'react-test-renderer';
+import Popover from './Popover';
+
+describe('Popover', () => {
+  it('renders correctly', () => {
+    const tree = renderer
+      .create(
+        <Popover placement="auto" content="Popover text">
+          <button>Button with Popover</button>
+        </Popover>
+      )
+      .toJSON();
+    expect(tree).toMatchSnapshot();
+  });
+});

+ 34 - 0
public/app/core/components/Tooltip/Popover.tsx

@@ -0,0 +1,34 @@
+import React from 'react';
+import withTooltip from './withTooltip';
+import { Target } from 'react-popper';
+
+interface IPopoverProps {
+  tooltipSetState: (prevState: object) => void;
+}
+
+class Popover extends React.Component<IPopoverProps, any> {
+  constructor(props) {
+    super(props);
+    this.toggleTooltip = this.toggleTooltip.bind(this);
+  }
+
+  toggleTooltip() {
+    const { tooltipSetState } = this.props;
+    tooltipSetState(prevState => {
+      return {
+        ...prevState,
+        show: !prevState.show,
+      };
+    });
+  }
+
+  render() {
+    return (
+      <Target className="popper__target" onClick={this.toggleTooltip}>
+        {this.props.children}
+      </Target>
+    );
+  }
+}
+
+export default withTooltip(Popover);

+ 16 - 0
public/app/core/components/Tooltip/Tooltip.jest.tsx

@@ -0,0 +1,16 @@
+import React from 'react';
+import renderer from 'react-test-renderer';
+import Tooltip from './Tooltip';
+
+describe('Tooltip', () => {
+  it('renders correctly', () => {
+    const tree = renderer
+      .create(
+        <Tooltip placement="auto" content="Tooltip text">
+          <a href="http://www.grafana.com">Link with tooltip</a>
+        </Tooltip>
+      )
+      .toJSON();
+    expect(tree).toMatchSnapshot();
+  });
+});

+ 45 - 0
public/app/core/components/Tooltip/Tooltip.tsx

@@ -0,0 +1,45 @@
+import React from 'react';
+import withTooltip from './withTooltip';
+import { Target } from 'react-popper';
+
+interface ITooltipProps {
+  tooltipSetState: (prevState: object) => void;
+}
+
+class Tooltip extends React.Component<ITooltipProps, any> {
+  constructor(props) {
+    super(props);
+    this.showTooltip = this.showTooltip.bind(this);
+    this.hideTooltip = this.hideTooltip.bind(this);
+  }
+
+  showTooltip() {
+    const { tooltipSetState } = this.props;
+    tooltipSetState(prevState => {
+      return {
+        ...prevState,
+        show: true,
+      };
+    });
+  }
+
+  hideTooltip() {
+    const { tooltipSetState } = this.props;
+    tooltipSetState(prevState => {
+      return {
+        ...prevState,
+        show: false,
+      };
+    });
+  }
+
+  render() {
+    return (
+      <Target className="popper__target" onMouseOver={this.showTooltip} onMouseOut={this.hideTooltip}>
+        {this.props.children}
+      </Target>
+    );
+  }
+}
+
+export default withTooltip(Tooltip);

+ 16 - 0
public/app/core/components/Tooltip/__snapshots__/Popover.jest.tsx.snap

@@ -0,0 +1,16 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`Popover renders correctly 1`] = `
+<div
+  className="popper__manager"
+>
+  <div
+    className="popper__target"
+    onClick={[Function]}
+  >
+    <button>
+      Button with Popover
+    </button>
+  </div>
+</div>
+`;

+ 19 - 0
public/app/core/components/Tooltip/__snapshots__/Tooltip.jest.tsx.snap

@@ -0,0 +1,19 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`Tooltip renders correctly 1`] = `
+<div
+  className="popper__manager"
+>
+  <div
+    className="popper__target"
+    onMouseOut={[Function]}
+    onMouseOver={[Function]}
+  >
+    <a
+      href="http://www.grafana.com"
+    >
+      Link with tooltip
+    </a>
+  </div>
+</div>
+`;

+ 57 - 0
public/app/core/components/Tooltip/withTooltip.tsx

@@ -0,0 +1,57 @@
+import React from 'react';
+import { Manager, Popper, Arrow } from 'react-popper';
+
+interface IwithTooltipProps {
+  placement?: string;
+  content: string | ((props: any) => JSX.Element);
+}
+
+export default function withTooltip(WrappedComponent) {
+  return class extends React.Component<IwithTooltipProps, any> {
+    constructor(props) {
+      super(props);
+
+      this.setState = this.setState.bind(this);
+      this.state = {
+        placement: this.props.placement || 'auto',
+        show: false,
+      };
+    }
+
+    componentWillReceiveProps(nextProps) {
+      if (nextProps.placement && nextProps.placement !== this.state.placement) {
+        this.setState(prevState => {
+          return {
+            ...prevState,
+            placement: nextProps.placement,
+          };
+        });
+      }
+    }
+
+    renderContent(content) {
+      if (typeof content === 'function') {
+        // If it's a function we assume it's a React component
+        const ReactComponent = content;
+        return <ReactComponent />;
+      }
+      return content;
+    }
+
+    render() {
+      const { content } = this.props;
+
+      return (
+        <Manager className="popper__manager">
+          <WrappedComponent {...this.props} tooltipSetState={this.setState} />
+          {this.state.show ? (
+            <Popper placement={this.state.placement} className="popper">
+              {this.renderContent(content)}
+              <Arrow className="popper__arrow" />
+            </Popper>
+          ) : null}
+        </Manager>
+      );
+    }
+  };
+}

+ 92 - 91
public/sass/_grafana.scss

@@ -1,104 +1,105 @@
 // vendor
-@import "../vendor/css/timepicker.css";
-@import "../vendor/css/spectrum.css";
+@import '../vendor/css/timepicker.css';
+@import '../vendor/css/spectrum.css';
 
 // MIXINS
-@import "mixins/mixins";
-@import "mixins/animations";
-@import "mixins/buttons";
-@import "mixins/breakpoints";
-@import "mixins/grid";
-@import "mixins/grid-framework";
-@import "mixins/hover";
-@import "mixins/forms";
-@import "mixins/drop_element";
+@import 'mixins/mixins';
+@import 'mixins/animations';
+@import 'mixins/buttons';
+@import 'mixins/breakpoints';
+@import 'mixins/grid';
+@import 'mixins/grid-framework';
+@import 'mixins/hover';
+@import 'mixins/forms';
+@import 'mixins/drop_element';
 
 // BASE
-@import "base/normalize";
-@import "base/reboot";
-@import "base/type";
-@import "base/forms";
-@import "base/grid";
-@import "base/fonts";
-@import "base/code";
-@import "base/icons";
+@import 'base/normalize';
+@import 'base/reboot';
+@import 'base/type';
+@import 'base/forms';
+@import 'base/grid';
+@import 'base/fonts';
+@import 'base/code';
+@import 'base/icons';
 
 // UTILS
-@import "utils/utils";
-@import "utils/validation";
-@import "utils/angular";
-@import "utils/spacings";
-@import "utils/widths";
+@import 'utils/utils';
+@import 'utils/validation';
+@import 'utils/angular';
+@import 'utils/spacings';
+@import 'utils/widths';
 
 // LAYOUTS
-@import "layout/lists";
-@import "layout/page";
+@import 'layout/lists';
+@import 'layout/page';
 
 // COMPONENTS
-@import "components/scrollbar";
-@import "components/cards";
-@import "components/buttons";
-@import "components/navs";
-@import "components/tabs";
-@import "components/alerts";
-@import "components/switch";
-@import "components/tooltip";
-@import "components/tags";
-@import "components/panel_graph";
-@import "components/submenu";
-@import "components/panel_alertlist";
-@import "components/panel_dashlist";
-@import "components/panel_gettingstarted";
-@import "components/panel_pluginlist";
-@import "components/panel_singlestat";
-@import "components/panel_table";
-@import "components/panel_text";
-@import "components/panel_heatmap";
-@import "components/panel_add_panel";
-@import "components/settings_permissions";
-@import "components/tagsinput";
-@import "components/tables_lists";
-@import "components/search";
-@import "components/gf-form";
-@import "components/sidemenu";
-@import "components/navbar";
-@import "components/timepicker";
-@import "components/filter-controls";
-@import "components/filter-list";
-@import "components/filter-table";
-@import "components/old_stuff";
-@import "components/typeahead";
-@import "components/modals";
-@import "components/dropdown";
-@import "components/color_picker";
-@import "components/footer";
-@import "components/infobox";
-@import "components/shortcuts";
-@import "components/drop";
-@import "components/query_editor";
-@import "components/tabbed_view";
-@import "components/query_part";
-@import "components/jsontree";
-@import "components/edit_sidemenu";
-@import "components/row.scss";
-@import "components/json_explorer";
-@import "components/code_editor";
-@import "components/dashboard_grid";
-@import "components/dashboard_list";
-@import "components/page_header";
-@import "components/dashboard_settings";
-@import "components/empty_list_cta";
+@import 'components/scrollbar';
+@import 'components/cards';
+@import 'components/buttons';
+@import 'components/navs';
+@import 'components/tabs';
+@import 'components/alerts';
+@import 'components/switch';
+@import 'components/tooltip';
+@import 'components/tags';
+@import 'components/panel_graph';
+@import 'components/submenu';
+@import 'components/panel_alertlist';
+@import 'components/panel_dashlist';
+@import 'components/panel_gettingstarted';
+@import 'components/panel_pluginlist';
+@import 'components/panel_singlestat';
+@import 'components/panel_table';
+@import 'components/panel_text';
+@import 'components/panel_heatmap';
+@import 'components/panel_add_panel';
+@import 'components/settings_permissions';
+@import 'components/tagsinput';
+@import 'components/tables_lists';
+@import 'components/search';
+@import 'components/gf-form';
+@import 'components/sidemenu';
+@import 'components/navbar';
+@import 'components/timepicker';
+@import 'components/filter-controls';
+@import 'components/filter-list';
+@import 'components/filter-table';
+@import 'components/old_stuff';
+@import 'components/typeahead';
+@import 'components/modals';
+@import 'components/dropdown';
+@import 'components/color_picker';
+@import 'components/footer';
+@import 'components/infobox';
+@import 'components/shortcuts';
+@import 'components/drop';
+@import 'components/query_editor';
+@import 'components/tabbed_view';
+@import 'components/query_part';
+@import 'components/jsontree';
+@import 'components/edit_sidemenu';
+@import 'components/row.scss';
+@import 'components/json_explorer';
+@import 'components/code_editor';
+@import 'components/dashboard_grid';
+@import 'components/dashboard_list';
+@import 'components/page_header';
+@import 'components/dashboard_settings';
+@import 'components/empty_list_cta';
+@import 'components/popper';
 
 // PAGES
-@import "pages/login";
-@import "pages/dashboard";
-@import "pages/playlist";
-@import "pages/admin";
-@import "pages/alerting";
-@import "pages/history";
-@import "pages/plugins";
-@import "pages/signup";
-@import "pages/styleguide";
-@import "pages/errorpage";
-@import "old_responsive";
-@import "components/view_states.scss";
+@import 'pages/login';
+@import 'pages/dashboard';
+@import 'pages/playlist';
+@import 'pages/admin';
+@import 'pages/alerting';
+@import 'pages/history';
+@import 'pages/plugins';
+@import 'pages/signup';
+@import 'pages/styleguide';
+@import 'pages/errorpage';
+@import 'old_responsive';
+@import 'components/view_states.scss';

+ 79 - 0
public/sass/components/_popper.scss

@@ -0,0 +1,79 @@
+.popper {
+  position: absolute;
+  background: $tooltipBackground;
+  color: $tooltipColor;
+  width: 150px;
+  border-radius: 3px;
+  box-shadow: 0 0 2px rgba(0, 0, 0, 0.5);
+  padding: 10px;
+  text-align: center;
+}
+.popper .popper__arrow {
+  width: 0;
+  height: 0;
+  border-style: solid;
+  position: absolute;
+  margin: 5px;
+}
+
+.popper .popper__arrow {
+  border-color: $tooltipBackground;
+}
+
+.popper[data-placement^='top'] {
+  margin-bottom: 5px;
+}
+.popper[data-placement^='top'] .popper__arrow {
+  border-width: 5px 5px 0 5px;
+  border-left-color: transparent;
+  border-right-color: transparent;
+  border-bottom-color: transparent;
+  bottom: -5px;
+  left: calc(50% - 5px);
+  margin-top: 0;
+  margin-bottom: 0;
+}
+.popper[data-placement^='bottom'] {
+  margin-top: 5px;
+}
+.popper[data-placement^='bottom'] .popper__arrow {
+  border-width: 0 5px 5px 5px;
+  border-left-color: transparent;
+  border-right-color: transparent;
+  border-top-color: transparent;
+  top: -5px;
+  left: calc(50% - 5px);
+  margin-top: 0;
+  margin-bottom: 0;
+}
+.popper[data-placement^='right'] {
+  margin-left: 5px;
+}
+.popper[data-placement^='right'] .popper__arrow {
+  border-width: 5px 5px 5px 0;
+  border-left-color: transparent;
+  border-top-color: transparent;
+  border-bottom-color: transparent;
+  left: -5px;
+  top: calc(50% - 5px);
+  margin-left: 0;
+  margin-right: 0;
+}
+.popper[data-placement^='left'] {
+  margin-right: 5px;
+}
+.popper[data-placement^='left'] .popper__arrow {
+  border-width: 5px 0 5px 5px;
+  border-top-color: transparent;
+  border-right-color: transparent;
+  border-bottom-color: transparent;
+  right: -5px;
+  top: calc(50% - 5px);
+  margin-left: 0;
+  margin-right: 0;
+}
+
+.popper__target,
+.popper__manager {
+  display: inline-block;
+}

+ 11 - 0
yarn.lock

@@ -7504,6 +7504,10 @@ pn@^1.0.0:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/pn/-/pn-1.0.0.tgz#1cf5a30b0d806cd18f88fc41a6b5d4ad615b3ba9"
 
+popper.js@^1.12.5:
+  version "1.12.9"
+  resolved "https://registry.yarnpkg.com/popper.js/-/popper.js-1.12.9.tgz#0dfbc2dff96c451bb332edcfcfaaf566d331d5b3"
+
 posix-character-classes@^0.1.0:
   version "0.1.1"
   resolved "https://registry.yarnpkg.com/posix-character-classes/-/posix-character-classes-0.1.1.tgz#01eac0fe3b5af71a2a6c02feabb8c1fef7e00eab"
@@ -8123,6 +8127,13 @@ react-grid-layout@^0.16.1:
     react-draggable "^3.0.3"
     react-resizable "^1.7.5"
 
+react-popper@^0.7.5:
+  version "0.7.5"
+  resolved "https://registry.yarnpkg.com/react-popper/-/react-popper-0.7.5.tgz#71c25946f291db381231281f6b95729e8b801596"
+  dependencies:
+    popper.js "^1.12.5"
+    prop-types "^15.5.10"
+
 react-resizable@^1.7.5:
   version "1.7.5"
   resolved "https://registry.yarnpkg.com/react-resizable/-/react-resizable-1.7.5.tgz#83eb75bb3684da6989bbbf4f826e1470f0af902e"