Skip to content

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

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 18 commits into from
Sep 22, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions src/core_plugins/metrics/public/components/markdown_editor.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this still needed? It feels like this should only happen within the KuiCodeEditor abstraction

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good question. If we pull this inside KuiCodeEditor which modes do we import? All; just the ones we need at the moment, and people using this with new languages, will need to change KuiCodeEditor too?

Again: I would like to provide a proper code editor at some point, where this should be cleaned up, but I guess not in this first step. (We apparently also include some modes in webpackShims...)

import Select from 'react-select';
import createSelectHandler from '../lib/create_select_handler';
Expand All @@ -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' });

Expand Down Expand Up @@ -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"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are there use-cases for specifying other themes than github? If not we should pull it into KuiCodeEditor. I'd probably rename mode to language too.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This gets back to the question on what level this abstraction should be. In the long term I would agree with you, and rename those, but for now I would just pass them through, since we are currently passing all properties just to the editor, and I think if we start renaming we should make all properties explicit again (which for this first version we decided against).

width="100%"
Expand Down
36 changes: 36 additions & 0 deletions ui_framework/dist/ui_framework.css
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
7 changes: 7 additions & 0 deletions ui_framework/doc_site/src/services/routes/routes.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -141,6 +144,10 @@ const components = [{
name: 'Card',
component: CardExample,
hasReact: true,
}, {
name: 'CodeEditor',
component: CodeEditor,
hasReact: true
}, {
name: 'ColorPicker',
component: ColorPickerExample,
Expand Down
29 changes: 29 additions & 0 deletions ui_framework/doc_site/src/views/code_editor/code_editor.js
Original file line number Diff line number Diff line change
@@ -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')}
/>
);
}
}
40 changes: 40 additions & 0 deletions ui_framework/doc_site/src/views/code_editor/code_editor_example.js
Original file line number Diff line number Diff line change
@@ -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>
);
Original file line number Diff line number Diff line change
@@ -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>"`;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks really bad, and likely "impossible" to review if there are changes to the component.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

And what's up with XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was done as @cjcenizal suggested to render out the whole DOM of ACE, and not just the more or less empty wrapper element. I still have mixed feelings about rendering library code in general and compare it, but I would leave the level that we test this snapshot up to your discussion :-)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oof, I didn't realize it wasn't going to be formatted! It's not so bad when it's formatted, but then there are all the escape double-quotes, too... bah...

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

Copy link
Contributor

@cjcenizal cjcenizal Sep 22, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@kjbekkelund @timroes Which of these three options do you think makes the most sense? I think I'd prefer 2, but I can accept 1.

1) Use debug()

expect(mount(component).debug()).toMatchSnapshot();

Yields:

<KuiCodeEditor aria-label="aria-label" className="testClass1 testClass2" data-test-subj="test subject string">
  <div className="kuiCodeEditorWrapper" style={{...}}>
    <div className="kuiCodeEditorKeyboardHint" id={42} tabIndex="0" role="button" onClick={[Function]} onKeyDown={[Function]}>
      <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>
    <ReactAce aria-label="aria-label" className="testClass1 testClass2" data-test-subj="test subject string" onBlur={[Function]} name="brace-editor" focus={false} mode="" theme="" height="500px" width="500px" value="" fontSize={12} showGutter={true} onChange={{...}} onPaste={{...}} onLoad={{...}} onScroll={{...}} minLines={{...}} maxLines={{...}} readOnly={false} highlightActiveLine={true} showPrintMargin={true} tabSize={4} cursorStart={1} editorProps={{...}} setOptions={{...}} wrapEnabled={false} enableBasicAutocompletion={false} enableLiveAutocompletion={false}>
      <div id="brace-editor" style={{...}} />
    </ReactAce>
  </div>
</KuiCodeEditor>

2) Use prettified html()

// Note that this requires the `html` dependency, which the UI Framework already uses.
import html from 'html';
expect(html.prettyPrint(mount(component).html(), {
  indent_size: 2,
  unformatted: [], // Expand all tags, including spans
})).toMatchSnapshot();

Yields:

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

3) Use render(), AKA you're over-thinking it CJ

expect(render(component)).toMatchSnapshot();

Yields:

<div
  class="kuiCodeEditorWrapper"
>
  <div
    class="kuiCodeEditorKeyboardHint"
    id="42"
    role="button"
    tabindex="0"
  >
    <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;"
  />
</div>

28 changes: 28 additions & 0 deletions ui_framework/src/components/code_editor/_code_editor.scss
Original file line number Diff line number Diff line change
@@ -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 change: 1 addition & 0 deletions ui_framework/src/components/code_editor/_index.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
@import "code_editor";
96 changes: 96 additions & 0 deletions ui_framework/src/components/code_editor/code_editor.js
Original file line number Diff line number Diff line change
@@ -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 {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we add a propTypes declaration for onBlur, width, and height?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done


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}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What are the specific props allowed here? I think we should extract those from props and explicitly pass them in. And then in the root element, we should pass in {...props}:

// line 63
      <div
        className="kuiCodeEditorWrapper"
        style={{ width, height }}
        {...this.props}
      >

This way data-test-subj will get assigned to the root element so we can refer to it within functional tests. We'll also be able to assign things like aria-describedby and other attributes. This is a pattern we're using with other UI components in the framework.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I also thought about this, but decided against it for the following reasons (that are of course up for discussion):

  • AceEditor has quite some properties, that we would need to extract and pass down.
  • It also passes down all additional props to the ace editor itself. Especially for the mentioned aria attributes it would imho make more sense to apply them to the editor, than the wrapper element (that is never focused).

I totally see the point with the testing data-test-subj that you want most likely to be applied to the wrapper element. We have now several options:

  1. We leave it as it is, and pass all properties to the editor, and just extract the ones we need for the wrapper element (which could also include the data-test-subj).
  2. We pass by default all properties down to the editor, and just extract the ones we need (like width and height) and offer a specific property (e.g. wrapperProps), that you can pass arbitrary properties to the wrapper element.
  3. We explicitly extract and pass all supported properties to AceEditor and all others to the wrapper (thus taking us the possibility to add additional (e.g. ARIA) attributes to the editor).
  4. We explicitly extract properties for the editor, but offer an additional editorProps property, which will be assigned to the editor instead of the wrapper element.

Which way would you prefer?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see your point! It sounds like #1 would be the simplest solution. What do you think?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reading it again after a day, I guess I would prefer 2. at the moment. That way we just pull out what we need, but still give the user a general way of attaching any property wherever she wants (using wrapperProps for the wrapper, or just applying them directly for the editor).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would 2 be also okay with you?

Copy link
Contributor

@cjcenizal cjcenizal Sep 21, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

After discussion on Zoom, we've decided to go with option 1.

ref={this.aceEditorRef}
onBlur={this.onBlurAce}
/>
</div>
);
}
}

KuiCodeEditor.propTypes = {
height: PropTypes.string,
onBlur: PropTypes.func,
width: PropTypes.string,
};
72 changes: 72 additions & 0 deletions ui_framework/src/components/code_editor/code_editor.test.js
Original file line number Diff line number Diff line change
@@ -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(() => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we put these tests inside another describe block, and add another test above it:

// You'll need to import this above.
import { requiredProps } from '../../test/required_props';

test('is rendered', () => {
  const component = <KuiCodeEditor />;
  expect(render(component)).toMatchSnapshot();
});

You'll need to run the tests again and commit the generated snapshot.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you also add additional tests like this for width and height?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wasn't sure if we should do snapshot testing here, since that would also test, that react-ace doesn't change what they render. I am not sure, whether it would be good to add snapshots test upon the code generated by a library.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also for the required_props we would first need to look how we handle the props as discussed in the above comment.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am not sure, whether it would be good to add snapshots test upon the code generated by a library.

I think it's OK to test library code if a component depends upon it. The test failure would be valuable because it will alert us to a change in DOM structure which would have otherwise gone unnoticed. This way, if some functional or other structure-reliant tests fail due to this change, we'll have a head's up and be able to clearly see how the structure changed. If the test failure is just noise and the change is superficial, then it's trivial to update the snapshot.

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 change: 1 addition & 0 deletions ui_framework/src/components/code_editor/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { KuiCodeEditor } from './code_editor';
4 changes: 4 additions & 0 deletions ui_framework/src/components/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,10 @@ export {
KuiCardGroup,
} from './card';

export {
KuiCodeEditor
} from './code_editor';

export {
KuiColorPicker,
} from './color_picker';
Expand Down
Loading