mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 17:59:23 -04:00
[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:
parent
404e12c2d2
commit
8f2ccc3a4a
14 changed files with 322 additions and 4 deletions
|
@ -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"
|
||||
|
|
|
@ -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%"
|
||||
|
|
36
ui_framework/dist/ui_framework.css
vendored
36
ui_framework/dist/ui_framework.css
vendored
|
@ -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;
|
||||
|
|
|
@ -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,
|
||||
|
|
29
ui_framework/doc_site/src/views/code_editor/code_editor.js
Normal file
29
ui_framework/doc_site/src/views/code_editor/code_editor.js
Normal 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')}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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>
|
||||
);
|
|
@ -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 you’re 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>"`;
|
28
ui_framework/src/components/code_editor/_code_editor.scss
Normal file
28
ui_framework/src/components/code_editor/_code_editor.scss
Normal 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;
|
||||
}
|
||||
}
|
1
ui_framework/src/components/code_editor/_index.scss
Normal file
1
ui_framework/src/components/code_editor/_index.scss
Normal file
|
@ -0,0 +1 @@
|
|||
@import "code_editor";
|
96
ui_framework/src/components/code_editor/code_editor.js
Normal file
96
ui_framework/src/components/code_editor/code_editor.js
Normal 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’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,
|
||||
};
|
72
ui_framework/src/components/code_editor/code_editor.test.js
Normal file
72
ui_framework/src/components/code_editor/code_editor.test.js
Normal 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);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
1
ui_framework/src/components/code_editor/index.js
Normal file
1
ui_framework/src/components/code_editor/index.js
Normal file
|
@ -0,0 +1 @@
|
|||
export { KuiCodeEditor } from './code_editor';
|
|
@ -27,6 +27,10 @@ export {
|
|||
KuiCardGroup,
|
||||
} from './card';
|
||||
|
||||
export {
|
||||
KuiCodeEditor
|
||||
} from './code_editor';
|
||||
|
||||
export {
|
||||
KuiColorPicker,
|
||||
} from './color_picker';
|
||||
|
|
|
@ -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";
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue