Fix ace a11y listener (#60639)

Also move the hook use_ui_ace_keyboard_mode.tsx into es_ui_shared

This was defined (and used) in both Console and SearchProfiler.

Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>
This commit is contained in:
Jean-Louis Leysens 2020-03-20 10:14:44 +01:00 committed by GitHub
parent ef0935ff45
commit b841526979
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 5 additions and 113 deletions

View file

@ -22,6 +22,7 @@ import { i18n } from '@kbn/i18n';
import { debounce } from 'lodash';
import { parse } from 'query-string';
import React, { CSSProperties, useCallback, useEffect, useRef, useState } from 'react';
import { useUIAceKeyboardMode } from '../../../../../../../es_ui_shared/public';
// @ts-ignore
import mappings from '../../../../../lib/mappings/mappings';
import { ConsoleMenu } from '../../../../components';
@ -34,7 +35,6 @@ import {
import * as senseEditor from '../../../../models/sense_editor';
import { autoIndent, getDocumentation } from '../console_menu_actions';
import { subscribeResizeChecker } from '../subscribe_console_resize_checker';
import { useUIAceKeyboardMode } from '../use_ui_ace_keyboard_mode';
import { applyCurrentSettings } from './apply_editor_settings';
import { registerCommands } from './keyboard_shortcuts';

View file

@ -29,3 +29,5 @@ export {
} from './request/np_ready_request';
export { indices } from './indices';
export { useUIAceKeyboardMode } from './use_ui_ace_keyboard_mode';

View file

@ -96,7 +96,7 @@ export function useUIAceKeyboardMode(aceTextAreaElement: HTMLTextAreaElement | n
}
return () => {
if (aceTextAreaElement) {
document.removeEventListener('keydown', documentKeyDownListener);
document.removeEventListener('keydown', documentKeyDownListener, { capture: true });
aceTextAreaElement.removeEventListener('keydown', aceKeydownListener);
const textAreaContainer = aceTextAreaElement.parentElement;
if (textAreaContainer && textAreaContainer.contains(overlayMountNode.current!)) {

View file

@ -8,7 +8,7 @@ import React, { memo, useRef, useEffect, useState } from 'react';
import { Editor as AceEditor } from 'brace';
import { initializeEditor } from './init_editor';
import { useUIAceKeyboardMode } from './use_ui_ace_keyboard_mode';
import { useUIAceKeyboardMode } from '../../../../../../src/plugins/es_ui_shared/public';
type EditorShim = ReturnType<typeof createEditorShim>;

View file

@ -1,110 +0,0 @@
/*
* 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';
import { i18n } from '@kbn/i18n';
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">
{i18n.translate('xpack.searchProfiler.aceAccessibilityOverlayInstructionEnter', {
defaultMessage: 'Press Enter to start editing.',
})}
</EuiText>
<EuiText size="s">
{i18n.translate('xpack.searchProfiler.aceAccessibilityOverlayInstructionExit', {
defaultMessage: `When you are done, press Escape to stop editing.`,
})}
</EuiText>
</>
);
export function useUIAceKeyboardMode(aceTextAreaElement: HTMLTextAreaElement | null) {
const overlayMountNode = useRef<HTMLDivElement | null>(null);
const autoCompleteVisibleRef = useRef<boolean>(false);
useEffect(() => {
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();
}
};
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]);
}