[UI Framework] Add KuiCodeEditor as react-ace replacement/wrapper (#14026)

* Create KuiCodeEditor component

* Add additional tests

* Add PropTypes for KuiCodeEditor

* Rename hintInactive to isHintActive

* Rename enableOverlay to stopEditing

* Rename and move configureAce method

* Rename onHintKeyDown to onKeyDownHint

* Fix broken configureAce call

* Add onBlur to editor example

* Regroup test cases

* Don't lose value in KuiCodeEditor example

* Remove window.alert, due to annoying behavior when switching tabs

* Remove unnecessary constructor

* Replace string ref by callback ref

* Add a snapshot test

* Move stop editing method

* Use mount to render editor during test

* Extract setState into method in example
This commit is contained in:
Tim Roes 2017-09-22 18:09:32 +03:00 committed by GitHub
parent 6569f1d864
commit bdc37be5b5
14 changed files with 322 additions and 4 deletions

View file

@ -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 (
<div className="vis_editor__markdown">
<div className="vis_editor__markdown-editor">
<AceEditor
<KuiCodeEditor
onLoad={this.handleOnLoad}
mode="markdown"
theme="github"

View file

@ -2,7 +2,6 @@ import PropTypes from 'prop-types';
import React, { Component } from 'react';
import SeriesEditor from '../series_editor';
import { IndexPattern } from '../index_pattern';
import AceEditor from 'react-ace';
import 'brace/mode/less';
import Select from 'react-select';
import createSelectHandler from '../lib/create_select_handler';
@ -11,6 +10,7 @@ import ColorPicker from '../color_picker';
import YesNo from '../yes_no';
import MarkdownEditor from '../markdown_editor';
import less from 'less/lib/less-browser';
import { KuiCodeEditor } from 'ui_framework/components';
import { htmlIdGenerator } from 'ui_framework/services';
const lessC = less(window, { env: 'production' });
@ -124,7 +124,7 @@ class MarkdownPanelConfig extends Component {
<div className="vis_editor__label">Custom CSS (supports Less)</div>
</div>
<div className="vis_editor__ace-editor">
<AceEditor
<KuiCodeEditor
mode="less"
theme="github"
width="100%"

View file

@ -666,6 +666,42 @@ main {
.kuiCardGroup--united .kuiCard + .kuiCard {
border-left: 1px solid #E0E0E0; }
.kuiCodeEditorWrapper {
position: relative; }
.kuiCodeEditorKeyboardHint {
position: absolute;
top: 0;
bottom: 0;
right: 0;
left: 0;
background: rgba(255, 255, 255, 0.7);
display: -webkit-box;
display: -webkit-flex;
display: -ms-flexbox;
display: flex;
-webkit-box-orient: vertical;
-webkit-box-direction: normal;
-webkit-flex-direction: column;
-ms-flex-direction: column;
flex-direction: column;
-webkit-box-pack: center;
-webkit-justify-content: center;
-ms-flex-pack: center;
justify-content: center;
-webkit-box-align: center;
-webkit-align-items: center;
-ms-flex-align: center;
align-items: center;
text-align: center;
opacity: 0; }
.kuiCodeEditorKeyboardHint:focus {
opacity: 1;
border: 2px solid #0079a5;
z-index: 1000; }
.kuiCodeEditorKeyboardHint.kuiCodeEditorKeyboardHint-isInactive {
display: none; }
.kuiCollapseButton {
-webkit-appearance: none;
-moz-appearance: none;

View file

@ -18,6 +18,9 @@ import ButtonExample
import CardExample
from '../../views/card/card_example';
import CodeEditor
from '../../views/code_editor/code_editor_example';
import CollapseButtonExample
from '../../views/collapse_button/collapse_button_example';
@ -141,6 +144,10 @@ const components = [{
name: 'Card',
component: CardExample,
hasReact: true,
}, {
name: 'CodeEditor',
component: CodeEditor,
hasReact: true
}, {
name: 'ColorPicker',
component: ColorPickerExample,

View file

@ -0,0 +1,29 @@
import React, { Component } from 'react';
import {
KuiCodeEditor
} from '../../../../components';
export default class extends Component {
state = {
value: ''
};
onChange = (value) => {
this.setState({ value });
};
render() {
return (
<KuiCodeEditor
mode="less"
theme="github"
width="100%"
value={this.state.value}
onChange={this.onChange}
setOptions={{ fontSize: '14px' }}
onBlur={() => console.log('KuiCodeEditor.onBlur() called')}
/>
);
}
}

View file

@ -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 => (
<GuidePage title={props.route.name}>
<GuideSection
title="Code Editor"
source={[{
type: GuideSectionTypes.JS,
code: codeEditorSource,
}]}
>
<GuideText>
<p>
The KuiCodeEditor component is a wrapper around <code>react-ace</code> (which
itself wraps the ACE code editor), that adds an accessible keyboard mode
to it. You should always use this component instead of <code>AceReact</code>.
</p>
<p>
All parameters, that you specify are passed down to the
underlying <code>AceReact</code> component.
</p>
</GuideText>
<GuideDemo>
<CodeEditor />
</GuideDemo>
</GuideSection>
</GuidePage>
);

View file

@ -0,0 +1,3 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`KuiCodeEditor is rendered 1`] = `"<div class=\\"kuiCodeEditorWrapper\\"><div class=\\"kuiCodeEditorKeyboardHint\\" id=\\"42\\" tabindex=\\"0\\" role=\\"button\\"><p class=\\"kuiText kuiVerticalRhythmSmall\\">Press Enter to start editing.</p><p class=\\"kuiText kuiVerticalRhythmSmall\\">When youre done, press Escape to stop editing.</p></div><div id=\\"brace-editor\\" style=\\"width: 500px; height: 500px;\\" class=\\" ace_editor ace-tm testClass1 testClass2\\"><textarea class=\\"ace_text-input\\" wrap=\\"off\\" autocorrect=\\"off\\" autocapitalize=\\"off\\" spellcheck=\\"false\\" style=\\"opacity: 0;\\" tabindex=\\"-1\\"></textarea><div class=\\"ace_gutter\\"><div class=\\"ace_layer ace_gutter-layer ace_folding-enabled\\"></div><div class=\\"ace_gutter-active-line\\"></div></div><div class=\\"ace_scroller\\"><div class=\\"ace_content\\"><div class=\\"ace_layer ace_print-margin-layer\\"><div class=\\"ace_print-margin\\" style=\\"left: 4px; visibility: visible;\\"></div></div><div class=\\"ace_layer ace_marker-layer\\"></div><div class=\\"ace_layer ace_text-layer\\" style=\\"padding: 0px 4px;\\"></div><div class=\\"ace_layer ace_marker-layer\\"></div><div class=\\"ace_layer ace_cursor-layer ace_hidden-cursors\\"><div class=\\"ace_cursor\\"></div></div></div></div><div class=\\"ace_scrollbar ace_scrollbar-v\\" style=\\"display: none; width: 20px;\\"><div class=\\"ace_scrollbar-inner\\" style=\\"width: 20px;\\"></div></div><div class=\\"ace_scrollbar ace_scrollbar-h\\" style=\\"display: none; height: 20px;\\"><div class=\\"ace_scrollbar-inner\\" style=\\"height: 20px;\\"></div></div><div style=\\"height: auto; width: auto; top: 0px; left: 0px; visibility: hidden; position: absolute; white-space: pre; overflow: hidden;\\"><div style=\\"height: auto; width: auto; top: 0px; left: 0px; visibility: hidden; position: absolute; white-space: pre; overflow: visible;\\"></div><div style=\\"height: auto; width: auto; top: 0px; left: 0px; visibility: hidden; position: absolute; white-space: pre; overflow: visible;\\">XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX</div></div></div></div>"`;

View file

@ -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;
}
}

View file

@ -0,0 +1 @@
@import "code_editor";

View file

@ -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 (
<div
className="kuiCodeEditorWrapper"
style={{ width, height }}
>
<div
className={classes}
id={this.idGenerator('codeEditor')}
ref={(hint) => { this.editorHint = hint; }}
tabIndex="0"
role="button"
onClick={this.startEditing}
onKeyDown={this.onKeyDownHint}
>
<p className="kuiText kuiVerticalRhythmSmall">
Press Enter to start editing.
</p>
<p className="kuiText kuiVerticalRhythmSmall">
When you&rsquo;re done, press Escape to stop editing.
</p>
</div>
<AceEditor
{...this.props}
ref={this.aceEditorRef}
onBlur={this.onBlurAce}
/>
</div>
);
}
}
KuiCodeEditor.propTypes = {
height: PropTypes.string,
onBlur: PropTypes.func,
width: PropTypes.string,
};

View file

@ -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(<KuiCodeEditor/>);
});
test('is rendered', () => {
const component = <KuiCodeEditor {...requiredProps}/>;
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(<KuiCodeEditor onBlur={blurSpy}/>);
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);
});
});
});

View file

@ -0,0 +1 @@
export { KuiCodeEditor } from './code_editor';

View file

@ -27,6 +27,10 @@ export {
KuiCardGroup,
} from './card';
export {
KuiCodeEditor
} from './code_editor';
export {
KuiColorPicker,
} from './color_picker';

View file

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