diff --git a/x-pack/legacy/plugins/searchprofiler/public/legacy.ts b/x-pack/legacy/plugins/searchprofiler/public/legacy.ts index b8da1e15266dae..471ff1a61fe104 100644 --- a/x-pack/legacy/plugins/searchprofiler/public/legacy.ts +++ b/x-pack/legacy/plugins/searchprofiler/public/legacy.ts @@ -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'; @@ -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); @@ -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: { diff --git a/x-pack/legacy/plugins/searchprofiler/public/np_ready/application/editor/editor.tsx b/x-pack/legacy/plugins/searchprofiler/public/np_ready/application/editor/editor.tsx index b3e69f1094e145..0b92dfcec80158 100644 --- a/x-pack/legacy/plugins/searchprofiler/public/np_ready/application/editor/editor.tsx +++ b/x-pack/legacy/plugins/searchprofiler/public/np_ready/application/editor/editor.tsx @@ -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; @@ -16,9 +17,12 @@ export interface Props { export const Editor = ({ licenseEnabled }: Props) => { const containerRef = useRef(null as any); const editorInstanceRef = useRef(null as any); + const [textArea, setTextArea] = useState(null); + useUIAceKeyboardMode(textArea); useEffect(() => { const divEl = containerRef.current; editorInstanceRef.current = initializeEditor({ el: divEl, licenseEnabled }); + setTextArea(containerRef.current!.querySelector('textarea')); }); return
; }; diff --git a/x-pack/legacy/plugins/searchprofiler/public/np_ready/application/editor/use_ui_ace_keyboard_mode.tsx b/x-pack/legacy/plugins/searchprofiler/public/np_ready/application/editor/use_ui_ace_keyboard_mode.tsx new file mode 100644 index 00000000000000..e3cb1147dbc7ed --- /dev/null +++ b/x-pack/legacy/plugins/searchprofiler/public/np_ready/application/editor/use_ui_ace_keyboard_mode.tsx @@ -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 + <> + Press Enter to start editing. + When you’re done, press Escape to stop editing. + +); + +export function useUIAceKeyboardMode(aceTextAreaElement: HTMLTextAreaElement | null) { + const overlayMountNode = useRef(null); + const autoCompleteVisibleRef = useRef(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('.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(, 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]); +} diff --git a/x-pack/legacy/plugins/searchprofiler/public/np_ready/plugin.ts b/x-pack/legacy/plugins/searchprofiler/public/np_ready/plugin.ts index a1d9100d341937..3a804b3e6d6acd 100644 --- a/x-pack/legacy/plugins/searchprofiler/public/np_ready/plugin.ts +++ b/x-pack/legacy/plugins/searchprofiler/public/np_ready/plugin.ts @@ -45,7 +45,6 @@ export class SearchProfilerUIPlugin implements Plugin { I18nContext, notifications, formatAngularHttpError, - el, }); }, });