diff --git a/x-pack/plugins/ml/public/components/form_label/__snapshots__/form_label.test.js.snap b/x-pack/plugins/ml/public/components/form_label/__snapshots__/form_label.test.js.snap
new file mode 100644
index 000000000000..057ed54b10a0
--- /dev/null
+++ b/x-pack/plugins/ml/public/components/form_label/__snapshots__/form_label.test.js.snap
@@ -0,0 +1,28 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`FormLabel Basic initialization 1`] = `
+
+
+
+
+`;
+
+exports[`FormLabel Full initialization 1`] = `
+
+
+
+
+`;
diff --git a/x-pack/plugins/ml/public/components/form_label/form_label.js b/x-pack/plugins/ml/public/components/form_label/form_label.js
index f549259b4e68..efdbf6b39265 100644
--- a/x-pack/plugins/ml/public/components/form_label/form_label.js
+++ b/x-pack/plugins/ml/public/components/form_label/form_label.js
@@ -4,34 +4,46 @@
* you may not use this file except in compliance with the Elastic License.
*/
+import './styles/main.less';
+import PropTypes from 'prop-types';
+import React, { Component } from 'react';
-import { uiModules } from 'ui/modules';
-const module = uiModules.get('apps/ml');
+import { JsonTooltip } from '../json_tooltip/json_tooltip';
-// directive for creating a form label including a hoverable icon
-// to provide additional information in a tooltip. label and tooltip
+// Component for creating a form label including a hoverable icon
+// to provide additional information in a tooltip. Label and tooltip
// text elements get unique ids based on label-id so they can be
// referenced by attributes, for example:
//
-// Label Text
+// Label Text
//
-module.directive('mlFormLabel', function () {
- return {
- scope: {
- labelId: '@',
- tooltipAppendToBody: '@'
- },
- restrict: 'E',
- replace: false,
- transclude: true,
- template: `
-
-
- `
- };
-});
+//
+// Writing this as a class based component because stateless components
+// cannot use ref(). Once angular is completely gone this can be rewritten
+// as a function stateless component.
+export class FormLabel extends Component {
+ constructor(props) {
+ super(props);
+ this.labelRef = React.createRef();
+ }
+ render() {
+ // labelClassName is used so we can override the class with 'kuiFormLabel'
+ // when used in an angular context. Once the component is no longer used from
+ // within angular, this prop can be removed and the className can be hardcoded.
+ const { labelId, labelClassName = 'euiFormLabel', children } = this.props;
+ return (
+
+
+
+
+ );
+ }
+}
+FormLabel.propTypes = {
+ labelId: PropTypes.string
+};
diff --git a/x-pack/plugins/ml/public/components/form_label/form_label.test.js b/x-pack/plugins/ml/public/components/form_label/form_label.test.js
new file mode 100644
index 000000000000..70b6983209c9
--- /dev/null
+++ b/x-pack/plugins/ml/public/components/form_label/form_label.test.js
@@ -0,0 +1,32 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { shallow } from 'enzyme';
+import React from 'react';
+
+import { FormLabel } from './form_label';
+
+describe('FormLabel', () => {
+
+ test('Basic initialization', () => {
+ const wrapper = shallow();
+ const props = wrapper.props();
+ expect(props.labelId).toBeUndefined();
+ expect(wrapper.find('label').text()).toBe('');
+ expect(wrapper).toMatchSnapshot();
+ });
+
+ test('Full initialization', () => {
+ const labelId = 'uid';
+ const labelText = 'Label Text';
+ const wrapper = shallow({labelText});
+
+ const labelElement = wrapper.find('label');
+ expect(labelElement.props().id).toBe(`ml_aria_label_${labelId}`);
+ expect(labelElement.text()).toBe(labelText);
+ expect(wrapper).toMatchSnapshot();
+ });
+});
diff --git a/x-pack/plugins/ml/public/components/form_label/form_label_directive.js b/x-pack/plugins/ml/public/components/form_label/form_label_directive.js
new file mode 100644
index 000000000000..85e004d4759a
--- /dev/null
+++ b/x-pack/plugins/ml/public/components/form_label/form_label_directive.js
@@ -0,0 +1,52 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+
+
+import React from 'react';
+import ReactDOM from 'react-dom';
+
+import angular from 'angular';
+import { uiModules } from 'ui/modules';
+const module = uiModules.get('apps/ml', ['react']);
+
+import { FormLabel } from './form_label';
+
+// directive for creating a form label including a hoverable icon
+// to provide additional information in a tooltip. label and tooltip
+// text elements get unique ids based on label-id so they can be
+// referenced by attributes, for example:
+//
+// Label Text
+//
+module.directive('mlFormLabel', function () {
+ return {
+ scope: {
+ labelId: '@'
+ },
+ restrict: 'E',
+ replace: false,
+ transclude: true,
+ link: (scope, element, attrs, ctrl, transclude) => {
+ const props = {
+ labelId: scope.labelId,
+ labelClassName: 'kuiFormLabel',
+ // transclude the label text/elements from the angular template
+ // to the labelRef from the react component.
+ ref: c => angular.element(c.labelRef.current).append(transclude())
+ };
+
+ ReactDOM.render(
+ React.createElement(FormLabel, props),
+ element[0]
+ );
+ }
+ };
+});
diff --git a/x-pack/plugins/ml/public/components/form_label/index.js b/x-pack/plugins/ml/public/components/form_label/index.js
index 38f165d16c17..f850e7b7a2fd 100644
--- a/x-pack/plugins/ml/public/components/form_label/index.js
+++ b/x-pack/plugins/ml/public/components/form_label/index.js
@@ -6,5 +6,5 @@
-import './form_label';
+import './form_label_directive';
import './styles/main.less';
diff --git a/x-pack/plugins/ml/public/components/json_tooltip/__snapshots__/json_tooltip.test.js.snap b/x-pack/plugins/ml/public/components/json_tooltip/__snapshots__/json_tooltip.test.js.snap
new file mode 100644
index 000000000000..03270b82e927
--- /dev/null
+++ b/x-pack/plugins/ml/public/components/json_tooltip/__snapshots__/json_tooltip.test.js.snap
@@ -0,0 +1,54 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`JsonTooltip Initialization with a non-existing tooltip attribute doesn't throw an error 1`] = `
+
+
+
+
+`;
+
+exports[`JsonTooltip Initialize with existing tooltip attribute 1`] = `
+
+
+
+ Unique identifier for job, can use lowercase alphanumeric and underscores.
+
+
+`;
+
+exports[`JsonTooltip Plain initialization doesn't throw an error 1`] = `
+
+
+
+
+`;
diff --git a/x-pack/plugins/ml/public/components/json_tooltip/index.js b/x-pack/plugins/ml/public/components/json_tooltip/index.js
index dccfc3e7a2a7..08dd2f828586 100644
--- a/x-pack/plugins/ml/public/components/json_tooltip/index.js
+++ b/x-pack/plugins/ml/public/components/json_tooltip/index.js
@@ -4,4 +4,4 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import './json_tooltip';
+import './json_tooltip_directive';
diff --git a/x-pack/plugins/ml/public/components/json_tooltip/json_tooltip.js b/x-pack/plugins/ml/public/components/json_tooltip/json_tooltip.js
index f8c3006d323c..7eb971239817 100644
--- a/x-pack/plugins/ml/public/components/json_tooltip/json_tooltip.js
+++ b/x-pack/plugins/ml/public/components/json_tooltip/json_tooltip.js
@@ -6,58 +6,29 @@
-// the tooltip descriptions are located in tooltips.json
-
+// component for placing an icon with a popover tooltip anywhere on a page
+// the id will match an entry in tooltips.json
import tooltips from './tooltips.json';
import './styles/main.less';
import PropTypes from 'prop-types';
import React from 'react';
-import ReactDOM from 'react-dom';
import { EuiIconTip } from '@elastic/eui';
-import { uiModules } from 'ui/modules';
-const module = uiModules.get('apps/ml', ['react']);
-
-const JsonTooltip = ({ id, position, text }) => (
-
-
- {text}
-
-);
+export const JsonTooltip = ({ id, position }) => {
+ const text = (tooltips[id]) ? tooltips[id].text : '';
+ return (
+
+
+ {text}
+
+ );
+};
JsonTooltip.propTypes = {
id: PropTypes.string,
- position: PropTypes.string,
- text: PropTypes.string
+ position: PropTypes.string
};
-
-// directive for placing an i icon with a popover tooltip anywhere on a page
-// tooltip format:
-// the_id will match an entry in tooltips.json
-module.directive('mlInfoIcon', function () {
- return {
- scope: {
- id: '@mlInfoIcon',
- position: '@'
- },
- restrict: 'AE',
- replace: false,
- link: (scope, element) => {
- const props = {
- id: scope.id,
- position: scope.position,
- text: (tooltips[scope.id]) ? tooltips[scope.id].text : ''
- };
-
- ReactDOM.render(
- React.createElement(JsonTooltip, props),
- element[0]
- );
- }
- };
-
-});
diff --git a/x-pack/plugins/ml/public/components/json_tooltip/json_tooltip.test.js b/x-pack/plugins/ml/public/components/json_tooltip/json_tooltip.test.js
new file mode 100644
index 000000000000..451429e40799
--- /dev/null
+++ b/x-pack/plugins/ml/public/components/json_tooltip/json_tooltip.test.js
@@ -0,0 +1,36 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { shallow } from 'enzyme';
+import React from 'react';
+
+import { JsonTooltip } from './json_tooltip';
+import tooltips from './tooltips.json';
+
+describe('JsonTooltip', () => {
+
+ test(`Plain initialization doesn't throw an error`, () => {
+ const wrapper = shallow();
+ expect(wrapper).toMatchSnapshot();
+ });
+
+ test(`Initialization with a non-existing tooltip attribute doesn't throw an error`, () => {
+ const id = 'non_existing_attribute';
+ const wrapper = shallow();
+ expect(wrapper).toMatchSnapshot();
+ });
+
+ test('Initialize with existing tooltip attribute', () => {
+ const id = 'new_job_id';
+ const wrapper = shallow();
+
+ // test the rendered span element which should be referenced by aria-describedby
+ const span = wrapper.find('span.ml-info-tooltip-text');
+ expect(span.props().id).toBe(`ml_aria_description_${id}`);
+ expect(span.text()).toBe(tooltips[id].text);
+ expect(wrapper).toMatchSnapshot();
+ });
+});
diff --git a/x-pack/plugins/ml/public/components/json_tooltip/json_tooltip_directive.js b/x-pack/plugins/ml/public/components/json_tooltip/json_tooltip_directive.js
new file mode 100644
index 000000000000..e37cb602a635
--- /dev/null
+++ b/x-pack/plugins/ml/public/components/json_tooltip/json_tooltip_directive.js
@@ -0,0 +1,40 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+
+
+import React from 'react';
+import ReactDOM from 'react-dom';
+
+import { uiModules } from 'ui/modules';
+const module = uiModules.get('apps/ml', ['react']);
+
+import { JsonTooltip } from './json_tooltip';
+
+// directive for placing an i icon with a popover tooltip anywhere on a page
+// tooltip format:
+// the_id will match an entry in tooltips.json
+module.directive('mlInfoIcon', function () {
+ return {
+ scope: {
+ id: '@mlInfoIcon',
+ position: '@'
+ },
+ restrict: 'AE',
+ replace: false,
+ link: (scope, element) => {
+ const props = {
+ id: scope.id,
+ position: scope.position
+ };
+
+ ReactDOM.render(
+ React.createElement(JsonTooltip, props),
+ element[0]
+ );
+ }
+ };
+});
diff --git a/x-pack/plugins/ml/public/components/json_tooltip/styles/main.less b/x-pack/plugins/ml/public/components/json_tooltip/styles/main.less
index fd07b33add91..3e913533d458 100644
--- a/x-pack/plugins/ml/public/components/json_tooltip/styles/main.less
+++ b/x-pack/plugins/ml/public/components/json_tooltip/styles/main.less
@@ -1,4 +1,4 @@
-i[ml-info-icon] {
+.ml-info-icon {
color: #888;
margin: 0 4px;
transition: color 0.15s;
@@ -14,7 +14,7 @@ i[ml-info-icon] {
}
}
-i[ml-info-icon]:hover {
+.ml-info-icon:hover {
color: #444;
transition: color 0.15s 0.15s;
}