diff --git a/src/core_plugins/metrics/public/components/markdown_editor.js b/src/core_plugins/metrics/public/components/markdown_editor.js
index 5bff9a69db9e..39edc0a4ab7f 100644
--- a/src/core_plugins/metrics/public/components/markdown_editor.js
+++ b/src/core_plugins/metrics/public/components/markdown_editor.js
@@ -7,7 +7,7 @@ import PropTypes from 'prop-types';
import React, { Component } from 'react';
import tickFormatter from './lib/tick_formatter';
import convertSeriesToVars from './lib/convert_series_to_vars';
-import AceEditor from 'react-ace';
+import { KuiCodeEditor } from 'ui_framework/components';
import _ from 'lodash';
import 'brace/mode/markdown';
import 'brace/theme/github';
@@ -96,7 +96,7 @@ class MarkdownEditor extends Component {
return (
-
Custom CSS (supports Less)
-
{
+ this.setState({ value });
+ };
+
+ render() {
+ return (
+ console.log('KuiCodeEditor.onBlur() called')}
+ />
+ );
+ }
+}
diff --git a/ui_framework/doc_site/src/views/code_editor/code_editor_example.js b/ui_framework/doc_site/src/views/code_editor/code_editor_example.js
new file mode 100644
index 000000000000..75f48ea22994
--- /dev/null
+++ b/ui_framework/doc_site/src/views/code_editor/code_editor_example.js
@@ -0,0 +1,40 @@
+import React from 'react';
+
+import {
+ GuideDemo,
+ GuidePage,
+ GuideSection,
+ GuideSectionTypes,
+ GuideText,
+} from '../../components';
+
+import CodeEditor from './code_editor';
+const codeEditorSource = require('!!raw!./code_editor');
+
+export default props => (
+
+
+
+
+ The KuiCodeEditor component is a wrapper around react-ace
(which
+ itself wraps the ACE code editor), that adds an accessible keyboard mode
+ to it. You should always use this component instead of AceReact
.
+
+
+ All parameters, that you specify are passed down to the
+ underlying AceReact
component.
+
+
+
+
+
+
+
+
+);
diff --git a/ui_framework/src/components/code_editor/__snapshots__/code_editor.test.js.snap b/ui_framework/src/components/code_editor/__snapshots__/code_editor.test.js.snap
new file mode 100644
index 000000000000..89e967697221
--- /dev/null
+++ b/ui_framework/src/components/code_editor/__snapshots__/code_editor.test.js.snap
@@ -0,0 +1,3 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`KuiCodeEditor is rendered 1`] = `"Press Enter to start editing.
When you’re done, press Escape to stop editing.
XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
"`;
diff --git a/ui_framework/src/components/code_editor/_code_editor.scss b/ui_framework/src/components/code_editor/_code_editor.scss
new file mode 100644
index 000000000000..9bb552dff23c
--- /dev/null
+++ b/ui_framework/src/components/code_editor/_code_editor.scss
@@ -0,0 +1,28 @@
+.kuiCodeEditorWrapper {
+ position: relative;
+}
+
+.kuiCodeEditorKeyboardHint {
+ position: absolute;
+ top: 0;
+ bottom: 0;
+ right: 0;
+ left: 0;
+ background: rgba(255, 255, 255, 0.7);
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ align-items: center;
+ text-align: center;
+ opacity: 0;
+
+ &:focus {
+ opacity: 1;
+ border: 2px solid $globalColorBlue;
+ z-index: 1000;
+ }
+
+ &.kuiCodeEditorKeyboardHint-isInactive {
+ display: none;
+ }
+}
diff --git a/ui_framework/src/components/code_editor/_index.scss b/ui_framework/src/components/code_editor/_index.scss
new file mode 100644
index 000000000000..dcbce0b6d211
--- /dev/null
+++ b/ui_framework/src/components/code_editor/_index.scss
@@ -0,0 +1 @@
+@import "code_editor";
diff --git a/ui_framework/src/components/code_editor/code_editor.js b/ui_framework/src/components/code_editor/code_editor.js
new file mode 100644
index 000000000000..f1de686e7fea
--- /dev/null
+++ b/ui_framework/src/components/code_editor/code_editor.js
@@ -0,0 +1,96 @@
+import React, { Component } from 'react';
+import PropTypes from 'prop-types';
+import classNames from 'classnames';
+import AceEditor from 'react-ace';
+
+import { htmlIdGenerator, keyCodes } from '../../../services';
+
+export class KuiCodeEditor extends Component {
+
+ state = {
+ isHintActive: true
+ };
+
+ idGenerator = htmlIdGenerator();
+
+ aceEditorRef = (aceEditor) => {
+ if (aceEditor) {
+ this.aceEditor = aceEditor;
+ aceEditor.editor.textInput.getElement().tabIndex = -1;
+ aceEditor.editor.textInput.getElement().addEventListener('keydown', this.onKeydownAce);
+ }
+ };
+
+ onKeydownAce = (ev) => {
+ if (ev.keyCode === keyCodes.ESCAPE) {
+ ev.preventDefault();
+ ev.stopPropagation();
+ this.stopEditing();
+ this.editorHint.focus();
+ }
+ }
+
+ onBlurAce = (...args) => {
+ this.stopEditing();
+ if (this.props.onBlur) {
+ this.props.onBlur(...args);
+ }
+ };
+
+ onKeyDownHint = (ev) => {
+ if (ev.keyCode === keyCodes.ENTER) {
+ ev.preventDefault();
+ this.startEditing();
+ }
+ };
+
+ startEditing = () => {
+ this.setState({ isHintActive: false });
+ this.aceEditor.editor.textInput.focus();
+ }
+
+ stopEditing() {
+ this.setState({ isHintActive: true });
+ }
+
+ render() {
+ const { width, height } = this.props;
+ const classes = classNames('kuiCodeEditorKeyboardHint', {
+ 'kuiCodeEditorKeyboardHint-isInactive': !this.state.isHintActive
+ });
+ return (
+
+
{ this.editorHint = hint; }}
+ tabIndex="0"
+ role="button"
+ onClick={this.startEditing}
+ onKeyDown={this.onKeyDownHint}
+ >
+
+ Press Enter to start editing.
+
+
+ When you’re done, press Escape to stop editing.
+
+
+
+
+ );
+ }
+}
+
+KuiCodeEditor.propTypes = {
+ height: PropTypes.string,
+ onBlur: PropTypes.func,
+ width: PropTypes.string,
+};
diff --git a/ui_framework/src/components/code_editor/code_editor.test.js b/ui_framework/src/components/code_editor/code_editor.test.js
new file mode 100644
index 000000000000..7b84dd7cb4e3
--- /dev/null
+++ b/ui_framework/src/components/code_editor/code_editor.test.js
@@ -0,0 +1,72 @@
+import React from 'react';
+import sinon from 'sinon';
+import { mount } from 'enzyme';
+import { KuiCodeEditor } from './code_editor';
+import { keyCodes } from '../../services';
+import { requiredProps } from '../../test/required_props';
+
+// Mock the htmlIdGenerator to generate predictable ids for snapshot tests
+jest.mock('../../services/accessibility/html_id_generator', () => ({
+ htmlIdGenerator: () => { return () => 42; },
+}));
+
+describe('KuiCodeEditor', () => {
+
+ let element;
+
+ beforeEach(() => {
+ element = mount();
+ });
+
+ test('is rendered', () => {
+ const component = ;
+ expect(mount(component).html()).toMatchSnapshot();
+ });
+
+ describe('hint element', () => {
+
+ test('should exist', () => {
+ expect(element.find('.kuiCodeEditorKeyboardHint').exists()).toBe(true);
+ });
+
+ test('should be tabable', () => {
+ expect(element.find('.kuiCodeEditorKeyboardHint').prop('tabIndex')).toBe('0');
+ });
+
+ test('should vanish when hit enter on it', () => {
+ const hint = element.find('.kuiCodeEditorKeyboardHint');
+ hint.simulate('keydown', { keyCode: keyCodes.ENTER });
+ expect(hint.hasClass('kuiCodeEditorKeyboardHint-isInactive')).toBe(true);
+ });
+
+ test('should be enabled after bluring the ui ace box', () => {
+ const hint = element.find('.kuiCodeEditorKeyboardHint');
+ hint.simulate('keydown', { keyCode: keyCodes.ENTER });
+ expect(hint.hasClass('kuiCodeEditorKeyboardHint-isInactive')).toBe(true);
+ element.instance().onBlurAce();
+ expect(hint.hasClass('kuiCodeEditorKeyboardHint-isInactive')).toBe(false);
+ });
+
+ });
+
+ describe('interaction', () => {
+
+ test('bluring the ace textbox should call a passed onBlur prop', () => {
+ const blurSpy = sinon.spy();
+ const el = mount();
+ el.instance().onBlurAce();
+ expect(blurSpy.called).toBe(true);
+ });
+
+ test('pressing escape in ace textbox will enable overlay', () => {
+ element.instance().onKeydownAce({
+ preventDefault: () => {},
+ stopPropagation: () => {},
+ keyCode: keyCodes.ESCAPE
+ });
+ expect(element.find('.kuiCodeEditorKeyboardHint').matchesElement(document.activeElement)).toBe(true);
+ });
+
+ });
+
+});
diff --git a/ui_framework/src/components/code_editor/index.js b/ui_framework/src/components/code_editor/index.js
new file mode 100644
index 000000000000..dd346795afbe
--- /dev/null
+++ b/ui_framework/src/components/code_editor/index.js
@@ -0,0 +1 @@
+export { KuiCodeEditor } from './code_editor';
diff --git a/ui_framework/src/components/index.js b/ui_framework/src/components/index.js
index 2b0b780eaee8..7ceb48ad7db8 100644
--- a/ui_framework/src/components/index.js
+++ b/ui_framework/src/components/index.js
@@ -27,6 +27,10 @@ export {
KuiCardGroup,
} from './card';
+export {
+ KuiCodeEditor
+} from './code_editor';
+
export {
KuiColorPicker,
} from './color_picker';
diff --git a/ui_framework/src/components/index.scss b/ui_framework/src/components/index.scss
index 6f432f2d266a..ba0b40277a45 100644
--- a/ui_framework/src/components/index.scss
+++ b/ui_framework/src/components/index.scss
@@ -18,6 +18,7 @@
@import "bar/index";
@import "button/index";
@import "card/index";
+@import "code_editor/index";
@import "collapse_button/index";
@import "color_picker/index";
@import "column/index";