Skip to content

Commit

Permalink
Fix type issues and fix a11y for ace editor
Browse files Browse the repository at this point in the history
  • Loading branch information
jloleysens committed Oct 23, 2019
1 parent 1ef208a commit 71df9e7
Show file tree
Hide file tree
Showing 4 changed files with 111 additions and 6 deletions.
9 changes: 5 additions & 4 deletions x-pack/legacy/plugins/searchprofiler/public/legacy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
*/

/* eslint-disable @kbn/eslint/no-restricted-paths */
import { npSetup, npStart } from 'ui/new_platform';
import { npSetup } from 'ui/new_platform';
import { I18nContext } from 'ui/i18n';
import uiRoutes from 'ui/routes';
import 'ui/capabilities/route_setup';
Expand All @@ -17,7 +17,7 @@ import { formatAngularHttpError } from 'ui/notify/lib';
import 'ui/autoload/all';
/* eslint-enable @kbn/eslint/no-restricted-paths */

import { NotificationsSetup } from 'src/core/public';
import { NotificationsSetup, ApplicationSetup } from 'src/core/public';
import { plugin } from './np_ready';

const pluginInstance = plugin({} as any);
Expand All @@ -38,17 +38,18 @@ uiRoutes.when('/dev_tools/searchprofiler', {
throw new Error(errorMessage);
}

const coreApplicationShim = {
const coreApplicationSetupShim: ApplicationSetup = {
register(app: any) {
const unmount = app.mount();
$scope.$on('$destroy', () => unmount());
},
registerMountContext: {} as any,
};

pluginInstance.setup(
{
...npSetup.core,
application: coreApplicationShim,
application: coreApplicationSetupShim,
},
{
__LEGACY: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,11 @@
* you may not use this file except in compliance with the Elastic License.
*/

import React, { useRef, useEffect } from 'react';
import React, { useRef, useEffect, useState } from 'react';
import { Editor as AceEditor } from 'brace';

import { initializeEditor } from './init_editor';
import { useUIAceKeyboardMode } from './use_ui_ace_keyboard_mode';

export interface Props {
licenseEnabled: boolean;
Expand All @@ -16,9 +17,12 @@ export interface Props {
export const Editor = ({ licenseEnabled }: Props) => {
const containerRef = useRef<HTMLDivElement>(null as any);
const editorInstanceRef = useRef<AceEditor>(null as any);
const [textArea, setTextArea] = useState<HTMLTextAreaElement | null>(null);
useUIAceKeyboardMode(textArea);
useEffect(() => {
const divEl = containerRef.current;
editorInstanceRef.current = initializeEditor({ el: divEl, licenseEnabled });
setTextArea(containerRef.current!.querySelector('textarea'));
});
return <div ref={containerRef} />;
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/

/**
* Copied from Console plugin
*/

import React, { useEffect, useRef } from 'react';
import * as ReactDOM from 'react-dom';
import { keyCodes, EuiText } from '@elastic/eui';

const OverlayText = () => (
// The point of this element is for accessibility purposes, so ignore eslint error
// in this case
//
// eslint-disable-next-line jsx-a11y/no-noninteractive-element-interactions
<>
<EuiText size="s">Press Enter to start editing.</EuiText>
<EuiText size="s">When you&rsquo;re done, press Escape to stop editing.</EuiText>
</>
);

export function useUIAceKeyboardMode(aceTextAreaElement: HTMLTextAreaElement | null) {
const overlayMountNode = useRef<HTMLDivElement | null>(null);
const autoCompleteVisibleRef = useRef<boolean>(false);

function onDismissOverlay(event: KeyboardEvent) {
if (event.keyCode === keyCodes.ENTER) {
event.preventDefault();
aceTextAreaElement!.focus();
}
}

function enableOverlay() {
if (overlayMountNode.current) {
overlayMountNode.current.focus();
}
}

const isAutoCompleteVisible = () => {
const autoCompleter = document.querySelector<HTMLDivElement>('.ace_autocomplete');
if (!autoCompleter) {
return false;
}
// The autoComplete is just hidden when it's closed, not removed from the DOM.
return autoCompleter.style.display !== 'none';
};

const documentKeyDownListener = () => {
autoCompleteVisibleRef.current = isAutoCompleteVisible();
};

const aceKeydownListener = (event: KeyboardEvent) => {
if (event.keyCode === keyCodes.ESCAPE && !autoCompleteVisibleRef.current) {
event.preventDefault();
event.stopPropagation();
enableOverlay();
}
};

useEffect(() => {
if (aceTextAreaElement) {
// We don't control HTML elements inside of ace so we imperatively create an element
// that acts as a container and insert it just before ace's textarea element
// so that the overlay lives at the correct spot in the DOM hierarchy.
overlayMountNode.current = document.createElement('div');
overlayMountNode.current.className = 'kbnUiAceKeyboardHint';
overlayMountNode.current.setAttribute('role', 'application');
overlayMountNode.current.tabIndex = 0;
overlayMountNode.current.addEventListener('focus', enableOverlay);
overlayMountNode.current.addEventListener('keydown', onDismissOverlay);

ReactDOM.render(<OverlayText />, overlayMountNode.current);

aceTextAreaElement.parentElement!.insertBefore(overlayMountNode.current, aceTextAreaElement);
aceTextAreaElement.setAttribute('tabindex', '-1');

// Order of events:
// 1. Document capture event fires first and we check whether an autocomplete menu is open on keydown
// (not ideal because this is scoped to the entire document).
// 2. Ace changes it's state (like hiding or showing autocomplete menu)
// 3. We check what button was pressed and whether autocomplete was visible then determine
// whether it should act like a dismiss or if we should display an overlay.
document.addEventListener('keydown', documentKeyDownListener, { capture: true });
aceTextAreaElement.addEventListener('keydown', aceKeydownListener);
}
return () => {
if (aceTextAreaElement) {
document.removeEventListener('keydown', documentKeyDownListener);
aceTextAreaElement.removeEventListener('keydown', aceKeydownListener);
const textAreaContainer = aceTextAreaElement.parentElement;
if (textAreaContainer && textAreaContainer.contains(overlayMountNode.current!)) {
textAreaContainer.removeChild(overlayMountNode.current!);
}
}
};
}, [aceTextAreaElement]);
}
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,6 @@ export class SearchProfilerUIPlugin implements Plugin {
I18nContext,
notifications,
formatAngularHttpError,
el,
});
},
});
Expand Down

0 comments on commit 71df9e7

Please sign in to comment.