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";