diff --git a/CHANGELOG.md b/CHANGELOG.md index e2d52e1bc6b..ac1dee74d2a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,6 @@ ## [`master`](https://github.com/elastic/eui/tree/master) +- Converted `EuiCodeEditor` to Typescript ([#2836](https://github.com/elastic/eui/pull/2836)) - Converted `EuiCode` and `EuiCodeBlock` and to Typescript ([#2835](https://github.com/elastic/eui/pull/2835)) - Converted `EuiFilePicker` to TypeScript ([#2832](https://github.com/elastic/eui/issues/2832)) - Exported `EuiSelectOptionProps` type ([#2830](https://github.com/elastic/eui/pull/2830)) diff --git a/src/components/code_editor/__snapshots__/code_editor.test.js.snap b/src/components/code_editor/__snapshots__/code_editor.test.tsx.snap similarity index 100% rename from src/components/code_editor/__snapshots__/code_editor.test.js.snap rename to src/components/code_editor/__snapshots__/code_editor.test.tsx.snap diff --git a/src/components/code_editor/code_editor.test.js b/src/components/code_editor/code_editor.test.tsx similarity index 92% rename from src/components/code_editor/code_editor.test.js rename to src/components/code_editor/code_editor.test.tsx index 7a3c8a75319..ac4eb6d4de0 100644 --- a/src/components/code_editor/code_editor.test.js +++ b/src/components/code_editor/code_editor.test.tsx @@ -1,6 +1,5 @@ import React from 'react'; -import sinon from 'sinon'; -import { mount } from 'enzyme'; +import { mount, ReactWrapper } from 'enzyme'; import { EuiCodeEditor } from './code_editor'; import { keyCodes } from '../../services'; import { @@ -46,7 +45,7 @@ describe('EuiCodeEditor', () => { }); describe('behavior', () => { - let component; + let component: ReactWrapper; beforeEach(() => { component = mount(); @@ -69,6 +68,7 @@ describe('EuiCodeEditor', () => { test('should be enabled when the ui ace box loses focus', () => { const hint = findTestSubject(component, 'codeEditorHint'); hint.simulate('keyup', { keyCode: keyCodes.ENTER }); + // @ts-ignore component.instance().onBlurAce(); expect( findTestSubject(component, 'codeEditorHint').getDOMNode() @@ -78,13 +78,15 @@ describe('EuiCodeEditor', () => { describe('interaction', () => { test('bluring the ace textbox should call a passed onBlur prop', () => { - const blurSpy = sinon.spy(); + const blurSpy = jest.fn().mockName('blurSpy'); const el = mount(); + // @ts-ignore el.instance().onBlurAce(); - expect(blurSpy.called).toBe(true); + expect(blurSpy).toHaveBeenCalled(); }); test('pressing escape in ace textbox will enable overlay', () => { + // @ts-ignore component.instance().onKeydownAce({ preventDefault: () => {}, stopPropagation: () => {}, diff --git a/src/components/code_editor/code_editor.js b/src/components/code_editor/code_editor.tsx similarity index 67% rename from src/components/code_editor/code_editor.js rename to src/components/code_editor/code_editor.tsx index faa13e98a78..68a91dd2db9 100644 --- a/src/components/code_editor/code_editor.js +++ b/src/components/code_editor/code_editor.tsx @@ -1,14 +1,17 @@ -import React, { Component } from 'react'; -import PropTypes from 'prop-types'; +import React, { Component, AriaAttributes, KeyboardEventHandler } from 'react'; import classNames from 'classnames'; -import AceEditor from 'react-ace'; +import AceEditor, { IAceEditorProps } from 'react-ace'; import { htmlIdGenerator, keyCodes } from '../../services'; import { EuiI18n } from '../i18n'; const DEFAULT_MODE = 'text'; -function setOrRemoveAttribute(element, attributeName, value) { +function setOrRemoveAttribute( + element: HTMLTextAreaElement, + attributeName: SupportedAriaAttribute, + value: SupportedAriaAttributes[SupportedAriaAttribute] +) { if (value === null || value === undefined) { element.removeAttribute(attributeName); } else { @@ -16,18 +19,54 @@ function setOrRemoveAttribute(element, attributeName, value) { } } -export class EuiCodeEditor extends Component { - state = { +type SupportedAriaAttribute = + | 'aria-label' + | 'aria-labelledby' + | 'aria-describedby'; +type SupportedAriaAttributes = Pick; + +export interface EuiCodeEditorProps extends SupportedAriaAttributes { + width?: string; + height?: string; + onBlur?: IAceEditorProps['onBlur']; + onFocus?: IAceEditorProps['onFocus']; + isReadOnly?: boolean; + setOptions: IAceEditorProps['setOptions']; + cursorStart?: number; + 'data-test-subj'?: string; + + /** + * Use string for a built-in mode or object for a custom mode + */ + mode?: IAceEditorProps['mode'] | object; +} + +export interface EuiCodeEditorState { + isHintActive: boolean; + isEditing: boolean; +} + +export class EuiCodeEditor extends Component< + EuiCodeEditorProps, + EuiCodeEditorState +> { + static defaultProps = { + setOptions: {}, + }; + + state: EuiCodeEditorState = { isHintActive: true, isEditing: false, }; idGenerator = htmlIdGenerator(); + aceEditor: AceEditor | null = null; + editorHint: HTMLDivElement | null = null; - aceEditorRef = aceEditor => { + aceEditorRef = (aceEditor: AceEditor | null) => { if (aceEditor) { this.aceEditor = aceEditor; - const textbox = aceEditor.editor.textInput.getElement(); + const textbox = aceEditor.editor.textInput.getElement() as HTMLTextAreaElement; textbox.tabIndex = -1; textbox.addEventListener('keydown', this.onKeydownAce); setOrRemoveAttribute(textbox, 'aria-label', this.props['aria-label']); @@ -44,38 +83,40 @@ export class EuiCodeEditor extends Component { } }; - onKeydownAce = ev => { - if (ev.keyCode === keyCodes.ESCAPE) { + onKeydownAce = (event: KeyboardEvent) => { + if (event.keyCode === keyCodes.ESCAPE) { // If the autocompletion context menu is open then we want to let ESCAPE close it but // **not** exit out of editing mode. - if (!this.aceEditor.editor.completer) { - ev.preventDefault(); - ev.stopPropagation(); + if (this.aceEditor !== null && !this.aceEditor.editor.completer) { + event.preventDefault(); + event.stopPropagation(); this.stopEditing(); - this.editorHint.focus(); + if (this.editorHint) { + this.editorHint.focus(); + } } } }; - onFocusAce = (...args) => { + onFocusAce: IAceEditorProps['onFocus'] = (event, editor) => { this.setState({ isEditing: true, }); if (this.props.onFocus) { - this.props.onFocus(...args); + this.props.onFocus(event, editor); } }; - onBlurAce = (...args) => { + onBlurAce: IAceEditorProps['onBlur'] = (event, editor) => { this.stopEditing(); if (this.props.onBlur) { - this.props.onBlur(...args); + this.props.onBlur(event, editor); } }; - onKeyDownHint = ev => { - if (ev.keyCode === keyCodes.ENTER) { - ev.preventDefault(); + onKeyDownHint: KeyboardEventHandler = event => { + if (event.keyCode === keyCodes.ENTER) { + event.preventDefault(); this.startEditing(); } }; @@ -84,7 +125,9 @@ export class EuiCodeEditor extends Component { this.setState({ isHintActive: false, }); - this.aceEditor.editor.textInput.focus(); + if (this.aceEditor !== null) { + this.aceEditor.editor.textInput.focus(); + } }; stopEditing() { @@ -99,7 +142,9 @@ export class EuiCodeEditor extends Component { } setCustomMode() { - this.aceEditor.editor.getSession().setMode(this.props.mode); + if (this.aceEditor !== null) { + this.aceEditor.editor.getSession().setMode(this.props.mode); + } } componentDidMount() { @@ -108,7 +153,7 @@ export class EuiCodeEditor extends Component { } } - componentDidUpdate(prevProps) { + componentDidUpdate(prevProps: EuiCodeEditorProps) { if (this.props.mode !== prevProps.mode && this.isCustomMode()) { this.setCustomMode(); } @@ -161,7 +206,7 @@ export class EuiCodeEditor extends Component { ref={hint => { this.editorHint = hint; }} - tabIndex="0" + tabIndex={0} role="button" onClick={this.startEditing} onKeyDown={this.onKeyDownHint} @@ -206,7 +251,7 @@ export class EuiCodeEditor extends Component {