mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 17:28:26 -04:00
[Security Solution][Endpoint] Respond console command input area support for text selection/replacement (#140447)
Respond Console support for command input area text selection and replacement via keyboard or copy paste. Users can now select text in the input area of the console and overwrite the selection with a new value (keyboard key) or delete/backspace on it.
This commit is contained in:
parent
3099159d02
commit
d8d1b1097b
6 changed files with 554 additions and 396 deletions
|
@ -10,7 +10,7 @@ import type { ConsoleTestSetup } from '../../mocks';
|
|||
import { getConsoleTestSetup } from '../../mocks';
|
||||
import type { ConsoleProps } from '../../types';
|
||||
import { INPUT_DEFAULT_PLACEHOLDER_TEXT } from '../console_state/state_update_handlers/handle_input_area_state';
|
||||
import { act, waitFor } from '@testing-library/react';
|
||||
import { act, waitFor, createEvent, fireEvent } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
|
||||
describe('When entering data into the Console input', () => {
|
||||
|
@ -34,8 +34,8 @@ describe('When entering data into the Console input', () => {
|
|||
return renderResult.getByTestId('test-inputPlaceholder').textContent;
|
||||
};
|
||||
|
||||
const getUserInputText = () => {
|
||||
return renderResult.getByTestId('test-cmdInput-userTextInput').textContent;
|
||||
const getLeftOfCursorText = () => {
|
||||
return renderResult.getByTestId('test-cmdInput-leftOfCursor').textContent;
|
||||
};
|
||||
|
||||
const getFooterText = () => {
|
||||
|
@ -57,24 +57,24 @@ describe('When entering data into the Console input', () => {
|
|||
render();
|
||||
|
||||
enterCommand('c', { inputOnly: true });
|
||||
expect(getUserInputText()).toEqual('c');
|
||||
expect(getLeftOfCursorText()).toEqual('c');
|
||||
|
||||
enterCommand('m', { inputOnly: true });
|
||||
expect(getUserInputText()).toEqual('cm');
|
||||
expect(getLeftOfCursorText()).toEqual('cm');
|
||||
});
|
||||
|
||||
it('should repeat letters if the user holds letter key down on the keyboard', () => {
|
||||
render();
|
||||
enterCommand('{a>5/}', { inputOnly: true, useKeyboard: true });
|
||||
expect(getUserInputText()).toEqual('aaaaa');
|
||||
expect(getLeftOfCursorText()).toEqual('aaaaa');
|
||||
});
|
||||
|
||||
it('should not display command key names in the input, when command keys are used', () => {
|
||||
render();
|
||||
enterCommand('{Meta>}', { inputOnly: true, useKeyboard: true });
|
||||
expect(getUserInputText()).toEqual('');
|
||||
expect(getLeftOfCursorText()).toEqual('');
|
||||
enterCommand('{Shift>}A{/Shift}', { inputOnly: true, useKeyboard: true });
|
||||
expect(getUserInputText()).toEqual('A');
|
||||
expect(getLeftOfCursorText()).toEqual('A');
|
||||
});
|
||||
|
||||
it('should display placeholder text when input area is blank', () => {
|
||||
|
@ -87,13 +87,13 @@ describe('When entering data into the Console input', () => {
|
|||
render();
|
||||
enterCommand('cm', { inputOnly: true });
|
||||
|
||||
expect(getInputPlaceholderText()).toEqual('');
|
||||
expect(renderResult.queryByTestId('test-inputPlaceholder')).toBeNull();
|
||||
});
|
||||
|
||||
it('should NOT display any hint test in footer if nothing is displayed', () => {
|
||||
it('should NOT display any hint text in footer if nothing is displayed', () => {
|
||||
render();
|
||||
|
||||
expect(getFooterText()?.trim()).toEqual('');
|
||||
expect(getFooterText()?.trim()).toBe('');
|
||||
});
|
||||
|
||||
it('should display hint when a known command is typed', () => {
|
||||
|
@ -180,7 +180,7 @@ describe('When entering data into the Console input', () => {
|
|||
it('should clear the input area and show placeholder with first item that is focused', async () => {
|
||||
await renderWithInputHistory('one');
|
||||
|
||||
expect(getUserInputText()).toEqual('');
|
||||
expect(getLeftOfCursorText()).toEqual('');
|
||||
|
||||
await waitFor(() => {
|
||||
expect(getInputPlaceholderText()).toEqual('cmd1 --help');
|
||||
|
@ -197,7 +197,7 @@ describe('When entering data into the Console input', () => {
|
|||
userEvent.keyboard('{Escape}');
|
||||
|
||||
await waitFor(() => {
|
||||
expect(getUserInputText()).toEqual('one');
|
||||
expect(getLeftOfCursorText()).toEqual('one');
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -211,7 +211,7 @@ describe('When entering data into the Console input', () => {
|
|||
userEvent.keyboard('{Enter}');
|
||||
|
||||
await waitFor(() => {
|
||||
expect(getUserInputText()).toEqual('cmd1 --help');
|
||||
expect(getLeftOfCursorText()).toEqual('cmd1 --help');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -221,6 +221,17 @@ describe('When entering data into the Console input', () => {
|
|||
return renderResult.getByTestId('test-cmdInput-rightOfCursor').textContent;
|
||||
};
|
||||
|
||||
const selectLeftOfCursorText = () => {
|
||||
// Select text to the left of the cursor
|
||||
const selection = window.getSelection();
|
||||
const range = document.createRange();
|
||||
|
||||
// Create a new range with the content that is to the left of the cursor
|
||||
range.selectNodeContents(renderResult.getByTestId('test-cmdInput-leftOfCursor'));
|
||||
selection!.removeAllRanges();
|
||||
selection!.addRange(range);
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
render();
|
||||
enterCommand('isolate', { inputOnly: true });
|
||||
|
@ -228,53 +239,53 @@ describe('When entering data into the Console input', () => {
|
|||
|
||||
it('should backspace and delete last character', () => {
|
||||
typeKeyboardKey('{backspace}');
|
||||
expect(getUserInputText()).toEqual('isolat');
|
||||
expect(getLeftOfCursorText()).toEqual('isolat');
|
||||
expect(getRightOfCursorText()).toEqual('');
|
||||
});
|
||||
|
||||
it('should clear the input if the user holds down the delete/backspace key', () => {
|
||||
typeKeyboardKey('{backspace>7/}');
|
||||
expect(getUserInputText()).toEqual('');
|
||||
expect(getLeftOfCursorText()).toEqual('');
|
||||
});
|
||||
|
||||
it('should move cursor to the left', () => {
|
||||
typeKeyboardKey('{ArrowLeft}');
|
||||
typeKeyboardKey('{ArrowLeft}');
|
||||
expect(getUserInputText()).toEqual('isola');
|
||||
expect(getLeftOfCursorText()).toEqual('isola');
|
||||
expect(getRightOfCursorText()).toEqual('te');
|
||||
});
|
||||
|
||||
it('should move cursor to the right', () => {
|
||||
typeKeyboardKey('{ArrowLeft}');
|
||||
typeKeyboardKey('{ArrowLeft}');
|
||||
expect(getUserInputText()).toEqual('isola');
|
||||
expect(getLeftOfCursorText()).toEqual('isola');
|
||||
expect(getRightOfCursorText()).toEqual('te');
|
||||
|
||||
typeKeyboardKey('{ArrowRight}');
|
||||
expect(getUserInputText()).toEqual('isolat');
|
||||
expect(getLeftOfCursorText()).toEqual('isolat');
|
||||
expect(getRightOfCursorText()).toEqual('e');
|
||||
});
|
||||
|
||||
it('should move cursor to the beginning', () => {
|
||||
typeKeyboardKey('{Home}');
|
||||
expect(getUserInputText()).toEqual('');
|
||||
expect(getLeftOfCursorText()).toEqual('');
|
||||
expect(getRightOfCursorText()).toEqual('isolate');
|
||||
});
|
||||
|
||||
it('should should move cursor to the end', () => {
|
||||
typeKeyboardKey('{Home}');
|
||||
expect(getUserInputText()).toEqual('');
|
||||
expect(getLeftOfCursorText()).toEqual('');
|
||||
expect(getRightOfCursorText()).toEqual('isolate');
|
||||
|
||||
typeKeyboardKey('{End}');
|
||||
expect(getUserInputText()).toEqual('isolate');
|
||||
expect(getLeftOfCursorText()).toEqual('isolate');
|
||||
expect(getRightOfCursorText()).toEqual('');
|
||||
});
|
||||
|
||||
it('should delete text', () => {
|
||||
typeKeyboardKey('{Home}');
|
||||
typeKeyboardKey('{Delete}');
|
||||
expect(getUserInputText()).toEqual('');
|
||||
expect(getLeftOfCursorText()).toEqual('');
|
||||
expect(getRightOfCursorText()).toEqual('solate');
|
||||
});
|
||||
|
||||
|
@ -282,7 +293,7 @@ describe('When entering data into the Console input', () => {
|
|||
typeKeyboardKey('{ArrowLeft}');
|
||||
typeKeyboardKey('{ArrowLeft}');
|
||||
|
||||
expect(getUserInputText()).toEqual('isola');
|
||||
expect(getLeftOfCursorText()).toEqual('isola');
|
||||
expect(getRightOfCursorText()).toEqual('te');
|
||||
|
||||
typeKeyboardKey('{enter}');
|
||||
|
@ -296,12 +307,51 @@ describe('When entering data into the Console input', () => {
|
|||
typeKeyboardKey('{Home}');
|
||||
typeKeyboardKey('{ArrowRight}');
|
||||
|
||||
expect(getUserInputText()).toEqual('c');
|
||||
expect(getLeftOfCursorText()).toEqual('c');
|
||||
expect(getRightOfCursorText()).toEqual('md1 ');
|
||||
|
||||
expect(getFooterText()).toEqual('Hit enter to execute');
|
||||
});
|
||||
|
||||
it('should replace selected text with key pressed', () => {
|
||||
typeKeyboardKey('{ArrowLeft>3/}'); // Press left arrow for 3 times
|
||||
selectLeftOfCursorText();
|
||||
typeKeyboardKey('a');
|
||||
|
||||
expect(getLeftOfCursorText()).toEqual('a');
|
||||
expect(getRightOfCursorText()).toEqual('ate');
|
||||
});
|
||||
|
||||
it('should replace selected text with content pasted', () => {
|
||||
typeKeyboardKey('{ArrowLeft>3/}'); // Press left arrow for 3 times
|
||||
selectLeftOfCursorText();
|
||||
|
||||
const inputCaptureEle = renderResult.getByTestId('test-keyCapture-input');
|
||||
|
||||
// Mocking the `DataTransfer` class since its not available in Jest test setup
|
||||
const clipboardData = {
|
||||
getData: () => 'I pasted this',
|
||||
} as unknown as DataTransfer;
|
||||
|
||||
const pasteEvent = createEvent.paste(inputCaptureEle, {
|
||||
clipboardData,
|
||||
});
|
||||
|
||||
fireEvent(inputCaptureEle, pasteEvent);
|
||||
|
||||
expect(getLeftOfCursorText()).toEqual('I pasted this');
|
||||
expect(getRightOfCursorText()).toEqual('ate');
|
||||
});
|
||||
|
||||
it('should delete selected text when delete key is pressed', () => {
|
||||
typeKeyboardKey('{ArrowLeft>3/}'); // Press left arrow for 3 times
|
||||
selectLeftOfCursorText();
|
||||
typeKeyboardKey('{Delete}');
|
||||
|
||||
expect(getLeftOfCursorText()).toEqual('');
|
||||
expect(getRightOfCursorText()).toEqual('ate');
|
||||
});
|
||||
|
||||
// FIXME:PT uncomment once task OLM task #4384 is implemented
|
||||
it.skip('should return original cursor position if input history is closed with no selection', async () => {
|
||||
typeKeyboardKey('{Enter}'); // add `isolate` to the input history
|
||||
|
@ -310,12 +360,12 @@ describe('When entering data into the Console input', () => {
|
|||
typeKeyboardKey('{Home}');
|
||||
typeKeyboardKey('{ArrowRight}');
|
||||
|
||||
expect(getUserInputText()).toEqual('r');
|
||||
expect(getLeftOfCursorText()).toEqual('r');
|
||||
expect(getRightOfCursorText()).toEqual('elease');
|
||||
|
||||
await showInputHistoryPopover();
|
||||
|
||||
expect(getUserInputText()).toEqual('');
|
||||
expect(getLeftOfCursorText()).toEqual('');
|
||||
expect(getRightOfCursorText()).toEqual('');
|
||||
|
||||
await waitFor(() => {
|
||||
|
@ -324,7 +374,7 @@ describe('When entering data into the Console input', () => {
|
|||
|
||||
userEvent.keyboard('{Escape}');
|
||||
|
||||
expect(getUserInputText()).toEqual('r');
|
||||
expect(getLeftOfCursorText()).toEqual('r');
|
||||
expect(getRightOfCursorText()).toEqual('elease');
|
||||
});
|
||||
|
||||
|
@ -336,12 +386,12 @@ describe('When entering data into the Console input', () => {
|
|||
typeKeyboardKey('{Home}');
|
||||
typeKeyboardKey('{ArrowRight}');
|
||||
|
||||
expect(getUserInputText()).toEqual('r');
|
||||
expect(getLeftOfCursorText()).toEqual('r');
|
||||
expect(getRightOfCursorText()).toEqual('elease');
|
||||
|
||||
await showInputHistoryPopover();
|
||||
|
||||
expect(getUserInputText()).toEqual('');
|
||||
expect(getLeftOfCursorText()).toEqual('');
|
||||
expect(getRightOfCursorText()).toEqual('');
|
||||
|
||||
await waitFor(() => {
|
||||
|
@ -350,7 +400,7 @@ describe('When entering data into the Console input', () => {
|
|||
|
||||
userEvent.keyboard('{Enter}');
|
||||
|
||||
expect(getUserInputText()).toEqual('isolate');
|
||||
expect(getLeftOfCursorText()).toEqual('isolate');
|
||||
expect(getRightOfCursorText()).toEqual('');
|
||||
});
|
||||
});
|
||||
|
|
|
@ -11,14 +11,14 @@ import type { CommonProps } from '@elastic/eui';
|
|||
import { EuiFlexGroup, EuiFlexItem, useResizeObserver, EuiButtonIcon } from '@elastic/eui';
|
||||
import styled from 'styled-components';
|
||||
import classNames from 'classnames';
|
||||
import { EnteredInput } from './lib/entered_input';
|
||||
import type { InputCaptureProps } from './components/input_capture';
|
||||
import { InputCapture } from './components/input_capture';
|
||||
import { useWithInputVisibleState } from '../../hooks/state_selectors/use_with_input_visible_state';
|
||||
import type { ConsoleDataState } from '../console_state/types';
|
||||
import { useInputHints } from './hooks/use_input_hints';
|
||||
import { InputPlaceholder } from './components/input_placeholder';
|
||||
import { useWithInputTextEntered } from '../../hooks/state_selectors/use_with_input_text_entered';
|
||||
import { InputAreaPopover } from './components/input_area_popover';
|
||||
import type { KeyCaptureProps } from './key_capture';
|
||||
import { KeyCapture } from './key_capture';
|
||||
import { useConsoleStateDispatch } from '../../hooks/state_selectors/use_console_state_dispatch';
|
||||
import { useTestIdGenerator } from '../../../../hooks/use_test_id_generator';
|
||||
import { useDataTestSubj } from '../../hooks/state_selectors/use_data_test_subj';
|
||||
|
@ -29,11 +29,7 @@ const CommandInputContainer = styled.div`
|
|||
padding: ${({ theme: { eui } }) => eui.euiSizeS};
|
||||
outline: ${({ theme: { eui } }) => eui.euiBorderThin};
|
||||
|
||||
.prompt {
|
||||
padding-right: 1ch;
|
||||
}
|
||||
|
||||
&.active {
|
||||
&:focus-within {
|
||||
border-bottom: ${({ theme: { eui } }) => eui.euiBorderThick};
|
||||
border-bottom-color: ${({ theme: { eui } }) => eui.euiColorPrimary};
|
||||
}
|
||||
|
@ -46,29 +42,33 @@ const CommandInputContainer = styled.div`
|
|||
white-space: break-spaces;
|
||||
}
|
||||
|
||||
.prompt {
|
||||
padding-right: 1ch;
|
||||
}
|
||||
|
||||
.cursor {
|
||||
display: inline-block;
|
||||
width: 1px;
|
||||
height: ${({ theme: { eui } }) => eui.euiLineHeight}em;
|
||||
background-color: ${({ theme: { eui } }) => eui.euiTextColor};
|
||||
background-color: ${({ theme }) => theme.eui.euiTextSubduedColor};
|
||||
}
|
||||
|
||||
animation: cursor-blink-animation 1s steps(5, start) infinite;
|
||||
-webkit-animation: cursor-blink-animation 1s steps(5, start) infinite;
|
||||
@keyframes cursor-blink-animation {
|
||||
to {
|
||||
visibility: hidden;
|
||||
}
|
||||
}
|
||||
@-webkit-keyframes cursor-blink-animation {
|
||||
to {
|
||||
visibility: hidden;
|
||||
}
|
||||
}
|
||||
&.hasFocus {
|
||||
.cursor {
|
||||
background-color: ${({ theme: { eui } }) => eui.euiTextColor};
|
||||
animation: cursor-blink-animation 1s steps(5, start) infinite;
|
||||
-webkit-animation: cursor-blink-animation 1s steps(5, start) infinite;
|
||||
|
||||
&.inactive {
|
||||
background-color: ${({ theme }) => theme.eui.euiTextSubduedColor} !important;
|
||||
animation: none;
|
||||
-webkit-animation: none;
|
||||
@keyframes cursor-blink-animation {
|
||||
to {
|
||||
visibility: hidden;
|
||||
}
|
||||
}
|
||||
@-webkit-keyframes cursor-blink-animation {
|
||||
to {
|
||||
visibility: hidden;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
@ -76,7 +76,7 @@ const CommandInputContainer = styled.div`
|
|||
export interface CommandInputProps extends CommonProps {
|
||||
prompt?: string;
|
||||
isWaiting?: boolean;
|
||||
focusRef?: KeyCaptureProps['focusRef'];
|
||||
focusRef?: InputCaptureProps['focusRef'];
|
||||
}
|
||||
|
||||
export const CommandInput = memo<CommandInputProps>(({ prompt = '', focusRef, ...commonProps }) => {
|
||||
|
@ -89,12 +89,8 @@ export const CommandInput = memo<CommandInputProps>(({ prompt = '', focusRef, ..
|
|||
const [commandToExecute, setCommandToExecute] = useState('');
|
||||
|
||||
const containerRef = useRef<HTMLDivElement | null>(null);
|
||||
const _focusRef: KeyCaptureProps['focusRef'] = useRef(null);
|
||||
|
||||
// TODO:PT what do I use this for? investigate
|
||||
const textDisplayRef = useRef<HTMLDivElement | null>(null);
|
||||
|
||||
const dimensions = useResizeObserver(containerRef.current);
|
||||
const _focusRef: InputCaptureProps['focusRef'] = useRef(null);
|
||||
|
||||
const keyCaptureFocusRef = focusRef || _focusRef;
|
||||
|
||||
|
@ -102,17 +98,10 @@ export const CommandInput = memo<CommandInputProps>(({ prompt = '', focusRef, ..
|
|||
return dimensions.width ? `${dimensions.width}px` : '92vw';
|
||||
}, [dimensions.width]);
|
||||
|
||||
const cursorClassName = useMemo(() => {
|
||||
return classNames({
|
||||
cursor: true,
|
||||
inactive: !isKeyInputBeingCaptured,
|
||||
});
|
||||
}, [isKeyInputBeingCaptured]);
|
||||
|
||||
const inputContainerClassname = useMemo(() => {
|
||||
return classNames({
|
||||
cmdInput: true,
|
||||
active: isKeyInputBeingCaptured,
|
||||
hasFocus: isKeyInputBeingCaptured,
|
||||
error: visibleState === 'error',
|
||||
});
|
||||
}, [isKeyInputBeingCaptured, visibleState]);
|
||||
|
@ -133,9 +122,9 @@ export const CommandInput = memo<CommandInputProps>(({ prompt = '', focusRef, ..
|
|||
[dispatch, textEntered, rightOfCursor.text]
|
||||
);
|
||||
|
||||
const handleKeyCaptureOnStateChange = useCallback<NonNullable<KeyCaptureProps['onStateChange']>>(
|
||||
(isCapturing) => {
|
||||
setIsKeyInputBeingCaptured(isCapturing);
|
||||
const handleOnChangeFocus = useCallback<NonNullable<InputCaptureProps['onChangeFocus']>>(
|
||||
(hasFocus) => {
|
||||
setIsKeyInputBeingCaptured(hasFocus);
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
@ -149,8 +138,8 @@ export const CommandInput = memo<CommandInputProps>(({ prompt = '', focusRef, ..
|
|||
[keyCaptureFocusRef]
|
||||
);
|
||||
|
||||
const handleKeyCapture = useCallback<KeyCaptureProps['onCapture']>(
|
||||
({ value, eventDetails }) => {
|
||||
const handleInputCapture = useCallback<InputCaptureProps['onCapture']>(
|
||||
({ value, selection, eventDetails }) => {
|
||||
const keyCode = eventDetails.keyCode;
|
||||
|
||||
// UP arrow key
|
||||
|
@ -165,107 +154,59 @@ export const CommandInput = memo<CommandInputProps>(({ prompt = '', focusRef, ..
|
|||
// Update the store with the updated text that was entered
|
||||
dispatch({
|
||||
type: 'updateInputTextEnteredState',
|
||||
payload: ({ rightOfCursor: prevRightOfCursor, textEntered: prevTextEntered }) => {
|
||||
let updatedTextEnteredState = prevTextEntered + value;
|
||||
let updatedRightOfCursor: ConsoleDataState['input']['rightOfCursor'] | undefined =
|
||||
prevRightOfCursor;
|
||||
payload: ({ textEntered: prevLeftOfCursor, rightOfCursor: prevRightOfCursor }) => {
|
||||
let inputText = new EnteredInput(prevLeftOfCursor, prevRightOfCursor.text);
|
||||
|
||||
const lengthOfTextEntered = updatedTextEnteredState.length;
|
||||
inputText.addValue(value ?? '', selection);
|
||||
|
||||
switch (keyCode) {
|
||||
// BACKSPACE
|
||||
// remove the last character from the text entered
|
||||
case 8:
|
||||
if (lengthOfTextEntered) {
|
||||
updatedTextEnteredState = updatedTextEnteredState.substring(
|
||||
0,
|
||||
lengthOfTextEntered - 1
|
||||
);
|
||||
}
|
||||
break;
|
||||
|
||||
// ENTER
|
||||
// Execute command and blank out the input area
|
||||
case 13:
|
||||
setCommandToExecute(updatedTextEnteredState + rightOfCursor.text);
|
||||
updatedTextEnteredState = '';
|
||||
updatedRightOfCursor = undefined;
|
||||
break;
|
||||
|
||||
// ARROW LEFT
|
||||
// Move cursor left (or more accurately - move text to the right of the cursor)
|
||||
case 37:
|
||||
updatedRightOfCursor = {
|
||||
...prevRightOfCursor,
|
||||
text:
|
||||
updatedTextEnteredState.charAt(lengthOfTextEntered - 1) + prevRightOfCursor.text,
|
||||
};
|
||||
updatedTextEnteredState = updatedTextEnteredState.substring(
|
||||
0,
|
||||
lengthOfTextEntered - 1
|
||||
);
|
||||
break;
|
||||
|
||||
// ARROW RIGHT
|
||||
// Move cursor right (or more accurately - move text to the left of the cursor)
|
||||
case 39:
|
||||
updatedRightOfCursor = {
|
||||
...prevRightOfCursor,
|
||||
text: prevRightOfCursor.text.substring(1),
|
||||
};
|
||||
updatedTextEnteredState = updatedTextEnteredState + prevRightOfCursor.text.charAt(0);
|
||||
break;
|
||||
|
||||
// HOME
|
||||
// Move cursor to the start of the input area
|
||||
// (or more accurately - move all text to the right of the cursor)
|
||||
case 36:
|
||||
updatedRightOfCursor = {
|
||||
...prevRightOfCursor,
|
||||
text: updatedTextEnteredState + prevRightOfCursor.text,
|
||||
};
|
||||
updatedTextEnteredState = '';
|
||||
break;
|
||||
|
||||
// END
|
||||
// Move cursor to the end of the input area
|
||||
// (or more accurately - move all text to the left of the cursor)
|
||||
case 35:
|
||||
updatedRightOfCursor = {
|
||||
...prevRightOfCursor,
|
||||
text: '',
|
||||
};
|
||||
updatedTextEnteredState = updatedTextEnteredState + prevRightOfCursor.text;
|
||||
inputText.backspaceChar(selection);
|
||||
break;
|
||||
|
||||
// DELETE
|
||||
// Remove the first character from the Right side of cursor
|
||||
case 46:
|
||||
if (prevRightOfCursor.text) {
|
||||
updatedRightOfCursor = {
|
||||
...prevRightOfCursor,
|
||||
text: prevRightOfCursor.text.substring(1),
|
||||
};
|
||||
}
|
||||
inputText.deleteChar(selection);
|
||||
break;
|
||||
|
||||
// ENTER = Execute command and blank out the input area
|
||||
case 13:
|
||||
setCommandToExecute(inputText.getFullText());
|
||||
inputText = new EnteredInput('', '');
|
||||
break;
|
||||
|
||||
// ARROW LEFT
|
||||
case 37:
|
||||
inputText.moveCursorTo('left');
|
||||
break;
|
||||
|
||||
// ARROW RIGHT
|
||||
case 39:
|
||||
inputText.moveCursorTo('right');
|
||||
break;
|
||||
|
||||
// HOME
|
||||
case 36:
|
||||
inputText.moveCursorTo('home');
|
||||
break;
|
||||
|
||||
// END
|
||||
case 35:
|
||||
inputText.moveCursorTo('end');
|
||||
break;
|
||||
}
|
||||
|
||||
return {
|
||||
textEntered: updatedTextEnteredState,
|
||||
rightOfCursor: updatedRightOfCursor,
|
||||
textEntered: inputText.getLeftOfCursorText(),
|
||||
rightOfCursor: { text: inputText.getRightOfCursorText() },
|
||||
};
|
||||
},
|
||||
});
|
||||
},
|
||||
[dispatch, rightOfCursor.text]
|
||||
[dispatch]
|
||||
);
|
||||
|
||||
const handleOnFocus = useCallback(() => {
|
||||
if (!isKeyInputBeingCaptured) {
|
||||
dispatch({ type: 'addFocusToKeyCapture' });
|
||||
}
|
||||
}, [dispatch, isKeyInputBeingCaptured]);
|
||||
|
||||
// Execute the command if one was ENTER'd.
|
||||
useEffect(() => {
|
||||
if (commandToExecute) {
|
||||
|
@ -281,8 +222,6 @@ export const CommandInput = memo<CommandInputProps>(({ prompt = '', focusRef, ..
|
|||
className={inputContainerClassname}
|
||||
onClick={handleTypingAreaClick}
|
||||
ref={containerRef}
|
||||
tabIndex={0}
|
||||
onFocus={handleOnFocus}
|
||||
data-test-subj={getTestId('cmdInput-container')}
|
||||
>
|
||||
<EuiFlexGroup
|
||||
|
@ -291,7 +230,6 @@ export const CommandInput = memo<CommandInputProps>(({ prompt = '', focusRef, ..
|
|||
alignItems="center"
|
||||
gutterSize="none"
|
||||
justifyContent="flexStart"
|
||||
ref={textDisplayRef}
|
||||
>
|
||||
{prompt && (
|
||||
<EuiFlexItem grow={false} data-test-subj={getTestId('cmdInput-prompt')}>
|
||||
|
@ -299,22 +237,30 @@ export const CommandInput = memo<CommandInputProps>(({ prompt = '', focusRef, ..
|
|||
</EuiFlexItem>
|
||||
)}
|
||||
<EuiFlexItem className="textEntered">
|
||||
<EuiFlexGroup
|
||||
responsive={false}
|
||||
alignItems="center"
|
||||
gutterSize="none"
|
||||
justifyContent="flexStart"
|
||||
<InputCapture
|
||||
onCapture={handleInputCapture}
|
||||
onChangeFocus={handleOnChangeFocus}
|
||||
focusRef={focusRef}
|
||||
>
|
||||
<EuiFlexItem grow={false}>
|
||||
<div data-test-subj={getTestId('cmdInput-userTextInput')}>{textEntered}</div>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<span className={cursorClassName} />
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<div data-test-subj={getTestId('cmdInput-rightOfCursor')}>{rightOfCursor.text}</div>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
<EuiFlexGroup
|
||||
responsive={false}
|
||||
alignItems="center"
|
||||
gutterSize="none"
|
||||
justifyContent="flexStart"
|
||||
>
|
||||
<EuiFlexItem grow={false}>
|
||||
<div data-test-subj={getTestId('cmdInput-leftOfCursor')}>{textEntered}</div>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<span className="cursor essentialAnimation" />
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<div data-test-subj={getTestId('cmdInput-rightOfCursor')}>
|
||||
{rightOfCursor.text}
|
||||
</div>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</InputCapture>
|
||||
<InputPlaceholder />
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
|
@ -329,12 +275,6 @@ export const CommandInput = memo<CommandInputProps>(({ prompt = '', focusRef, ..
|
|||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
|
||||
<KeyCapture
|
||||
onCapture={handleKeyCapture}
|
||||
focusRef={keyCaptureFocusRef}
|
||||
onStateChange={handleKeyCaptureOnStateChange}
|
||||
/>
|
||||
</CommandInputContainer>
|
||||
</InputAreaPopover>
|
||||
);
|
||||
|
|
|
@ -0,0 +1,280 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import type {
|
||||
KeyboardEventHandler,
|
||||
KeyboardEvent,
|
||||
MutableRefObject,
|
||||
PropsWithChildren,
|
||||
ClipboardEventHandler,
|
||||
} from 'react';
|
||||
import React, { memo, useCallback, useMemo, useRef } from 'react';
|
||||
import { pick } from 'lodash';
|
||||
import styled from 'styled-components';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { useTestIdGenerator } from '../../../../../hooks/use_test_id_generator';
|
||||
import { useDataTestSubj } from '../../../hooks/state_selectors/use_data_test_subj';
|
||||
|
||||
const ARIA_PLACEHOLDER_MESSAGE = i18n.translate(
|
||||
'xpack.securitySolution.inputCapture.ariaPlaceHolder',
|
||||
{ defaultMessage: 'Enter a command' }
|
||||
);
|
||||
|
||||
const deSelectTextOnPage = () => {
|
||||
const selection = getSelection();
|
||||
if (selection) {
|
||||
selection.removeAllRanges();
|
||||
}
|
||||
};
|
||||
|
||||
const InputCaptureContainer = styled.div`
|
||||
.focus-container {
|
||||
// Tried to find a way to not use '!important', but cant seem to figure
|
||||
// out right combination of pseudo selectors
|
||||
outline: none !important;
|
||||
}
|
||||
|
||||
.textSelectionBoundaryHelper {
|
||||
opacity: 0;
|
||||
position: absolute;
|
||||
top: -100vh;
|
||||
left: -100vw;
|
||||
}
|
||||
|
||||
.invisible-input {
|
||||
&,
|
||||
&:focus {
|
||||
border: none;
|
||||
outline: none;
|
||||
background-image: none;
|
||||
background-color: transparent;
|
||||
-webkit-box-shadow: none;
|
||||
-moz-box-shadow: none;
|
||||
box-shadow: none;
|
||||
animation: none !important;
|
||||
width: 1ch !important;
|
||||
position: absolute;
|
||||
opacity: 0;
|
||||
top: -100vh;
|
||||
left: -100vw;
|
||||
}
|
||||
`;
|
||||
|
||||
/**
|
||||
* Interface exposed by the `InputCapture` component that allows for interaction
|
||||
* with the component's focus/blur states.
|
||||
*/
|
||||
interface InputFocusInterface {
|
||||
focus: (force?: boolean) => void;
|
||||
blur: () => void;
|
||||
}
|
||||
|
||||
export type InputCaptureProps = PropsWithChildren<{
|
||||
onCapture: (params: {
|
||||
/** The keyboard key value that was entered by the user */
|
||||
value: string | undefined;
|
||||
/** Any text that is selected/highlighted when user clicked the keyboard key */
|
||||
selection: string;
|
||||
/** Keyboard control keys from the keyboard event */
|
||||
eventDetails: Pick<
|
||||
KeyboardEvent,
|
||||
'key' | 'altKey' | 'ctrlKey' | 'keyCode' | 'metaKey' | 'repeat' | 'shiftKey'
|
||||
>;
|
||||
}) => void;
|
||||
/** Sets an interface that allows interactions with this component's focus/blur states */
|
||||
focusRef?: MutableRefObject<InputFocusInterface | null>;
|
||||
/** Callback triggered whenever Focus/Blur events are triggered */
|
||||
onChangeFocus?: (hasFocus: boolean) => void;
|
||||
}>;
|
||||
|
||||
/**
|
||||
* Component that will capture keyboard and other user input (ex. paste) that
|
||||
* occur within this component
|
||||
*/
|
||||
export const InputCapture = memo<InputCaptureProps>(
|
||||
({ onCapture, focusRef, onChangeFocus, children }) => {
|
||||
const getTestId = useTestIdGenerator(useDataTestSubj());
|
||||
// Reference to the `<div>` that take in focus (`tabIndex`)
|
||||
const focusEleRef = useRef<HTMLDivElement | null>(null);
|
||||
|
||||
const hiddenInputEleRef = useRef<HTMLInputElement | null>(null);
|
||||
|
||||
const getTextSelection = useCallback((): string => {
|
||||
if (focusEleRef.current) {
|
||||
const selection = document.getSelection();
|
||||
|
||||
// Get the selected text and remove any new line breaks from it.
|
||||
// The input area does not allow for new line breaks and due to the markup, if user makes
|
||||
// a selection that also captures the cursor, then a new line break is included in the selection
|
||||
const selectionText = (selection?.toString() ?? '').replace(/[\r\n]/g, '');
|
||||
|
||||
const isSelectionWithinInputCapture =
|
||||
focusEleRef.current && selection
|
||||
? focusEleRef.current?.contains(selection.focusNode) &&
|
||||
focusEleRef.current?.contains(selection.anchorNode)
|
||||
: false;
|
||||
|
||||
if (!selection || selectionText.length === 0 || !isSelectionWithinInputCapture) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return selectionText;
|
||||
}
|
||||
|
||||
return '';
|
||||
}, []);
|
||||
|
||||
const handleOnKeyDown = useCallback<KeyboardEventHandler>(
|
||||
(ev) => {
|
||||
// allows for clipboard events to be captured via onPaste event handler
|
||||
if (ev.metaKey || ev.ctrlKey) {
|
||||
return;
|
||||
}
|
||||
|
||||
// checking to ensure that the key is not a control character. Control character's `.key`
|
||||
// are at least two characters long and because we are handling `onKeyDown` we know that
|
||||
// a printable `.key` will always be just one character long.
|
||||
const newValue = /^[\w\d]{2}/.test(ev.key) ? '' : ev.key;
|
||||
|
||||
const currentTextSelection = getTextSelection();
|
||||
|
||||
const eventDetails = pick(ev, [
|
||||
'key',
|
||||
'altKey',
|
||||
'ctrlKey',
|
||||
'keyCode',
|
||||
'metaKey',
|
||||
'repeat',
|
||||
'shiftKey',
|
||||
]);
|
||||
|
||||
onCapture({
|
||||
value: newValue,
|
||||
selection: currentTextSelection,
|
||||
eventDetails,
|
||||
});
|
||||
|
||||
if (currentTextSelection) {
|
||||
deSelectTextOnPage();
|
||||
}
|
||||
},
|
||||
[getTextSelection, onCapture]
|
||||
);
|
||||
|
||||
const handleOnPaste = useCallback<ClipboardEventHandler>(
|
||||
(ev) => {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
|
||||
// Get the data the user pasted as text and remove all new line breaks from it
|
||||
const value = ev.clipboardData.getData('text').replace(/[\r\n]/g, '');
|
||||
|
||||
const currentTextSelection = getTextSelection();
|
||||
|
||||
// hard-coded for use in onCapture and future keyboard functions
|
||||
const eventDetails = {
|
||||
altKey: false,
|
||||
ctrlKey: false,
|
||||
key: 'Meta',
|
||||
keyCode: 91,
|
||||
metaKey: true,
|
||||
repeat: false,
|
||||
shiftKey: false,
|
||||
};
|
||||
|
||||
onCapture({
|
||||
value,
|
||||
selection: currentTextSelection,
|
||||
eventDetails,
|
||||
});
|
||||
|
||||
if (currentTextSelection) {
|
||||
deSelectTextOnPage();
|
||||
}
|
||||
},
|
||||
[getTextSelection, onCapture]
|
||||
);
|
||||
|
||||
const handleOnFocus = useCallback(() => {
|
||||
if (onChangeFocus) {
|
||||
onChangeFocus(true);
|
||||
}
|
||||
}, [onChangeFocus]);
|
||||
|
||||
const handleOnBlur = useCallback(() => {
|
||||
if (onChangeFocus) {
|
||||
onChangeFocus(false);
|
||||
}
|
||||
}, [onChangeFocus]);
|
||||
|
||||
const focusInterface = useMemo<InputFocusInterface>(() => {
|
||||
return {
|
||||
focus: (force: boolean = false) => {
|
||||
// If user selected text and `force` is not true, then don't focus (else they lose selection)
|
||||
if (
|
||||
(!force && (window.getSelection()?.toString() ?? '').length > 0) ||
|
||||
document.activeElement === hiddenInputEleRef.current
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
hiddenInputEleRef.current?.focus();
|
||||
},
|
||||
|
||||
blur: () => {
|
||||
// only blur if the input has focus
|
||||
if (hiddenInputEleRef.current && document.activeElement === hiddenInputEleRef.current) {
|
||||
hiddenInputEleRef.current?.blur();
|
||||
}
|
||||
},
|
||||
};
|
||||
}, []);
|
||||
|
||||
if (focusRef) {
|
||||
focusRef.current = focusInterface;
|
||||
}
|
||||
|
||||
return (
|
||||
<InputCaptureContainer
|
||||
data-test-subj={getTestId('inputCapture')}
|
||||
onKeyDown={handleOnKeyDown}
|
||||
onPaste={handleOnPaste}
|
||||
>
|
||||
<div
|
||||
role="textbox"
|
||||
aria-placeholder={ARIA_PLACEHOLDER_MESSAGE}
|
||||
tabIndex={0}
|
||||
ref={focusEleRef}
|
||||
className="focus-container"
|
||||
data-test-subj={getTestId('keyCapture-input')}
|
||||
onBlur={handleOnBlur}
|
||||
onFocus={handleOnFocus}
|
||||
>
|
||||
{/*
|
||||
This div.textSelectionBoundaryHelper and the one below help to ensure that when the user
|
||||
selects the start or end of the input text, that the node that are returned in the
|
||||
`Selection` object for 'focusNode` and `anchorNode` are within the input capture area.
|
||||
*/}
|
||||
<div className="textSelectionBoundaryHelper"> </div>
|
||||
{children}
|
||||
<div className="textSelectionBoundaryHelper"> </div>
|
||||
<input
|
||||
ref={hiddenInputEleRef}
|
||||
type="text"
|
||||
value=""
|
||||
tabIndex={-1}
|
||||
onPaste={handleOnPaste}
|
||||
onChange={() => {}}
|
||||
spellCheck="false"
|
||||
className="invisible-input"
|
||||
/>
|
||||
</div>
|
||||
</InputCaptureContainer>
|
||||
);
|
||||
}
|
||||
);
|
||||
InputCapture.displayName = 'InputCapture';
|
|
@ -26,13 +26,17 @@ export const InputPlaceholder = memo(() => {
|
|||
const placeholder = useWithInputPlaceholder();
|
||||
const getTestId = useTestIdGenerator(useDataTestSubj());
|
||||
|
||||
if (fullTextEntered.length > 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<InputPlaceholderContainer
|
||||
size="s"
|
||||
className="eui-textTruncate"
|
||||
data-test-subj={getTestId('inputPlaceholder')}
|
||||
>
|
||||
<div className="eui-textTruncate">{fullTextEntered ? '' : placeholder}</div>
|
||||
<div className="eui-textTruncate">{placeholder}</div>
|
||||
</InputPlaceholderContainer>
|
||||
);
|
||||
});
|
||||
|
|
|
@ -1,214 +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
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import type {
|
||||
ClipboardEventHandler,
|
||||
FormEventHandler,
|
||||
KeyboardEventHandler,
|
||||
MutableRefObject,
|
||||
} from 'react';
|
||||
import React, { memo, useCallback, useMemo, useRef, useState } from 'react';
|
||||
import { pick } from 'lodash';
|
||||
import styled from 'styled-components';
|
||||
import { useTestIdGenerator } from '../../../../hooks/use_test_id_generator';
|
||||
import { useDataTestSubj } from '../../hooks/state_selectors/use_data_test_subj';
|
||||
|
||||
const NOOP = () => undefined;
|
||||
|
||||
const KeyCaptureContainer = styled.span`
|
||||
display: inline-block;
|
||||
position: absolute;
|
||||
width: 1px;
|
||||
height: 1em;
|
||||
left: -110vw;
|
||||
top: -110vh;
|
||||
overflow: hidden;
|
||||
|
||||
.invisible-input {
|
||||
&,
|
||||
&:focus {
|
||||
border: none;
|
||||
background-image: none;
|
||||
background-color: transparent;
|
||||
-webkit-box-shadow: none;
|
||||
-moz-box-shadow: none;
|
||||
box-shadow: none;
|
||||
animation: none !important;
|
||||
width: 1ch !important;
|
||||
position: absolute;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
interface KeyCaptureFocusInterface {
|
||||
focus: (force?: boolean) => void;
|
||||
blur: () => void;
|
||||
}
|
||||
|
||||
export interface KeyCaptureProps {
|
||||
onCapture: (params: {
|
||||
value: string | undefined;
|
||||
eventDetails: Pick<
|
||||
KeyboardEvent,
|
||||
'key' | 'altKey' | 'ctrlKey' | 'keyCode' | 'metaKey' | 'repeat' | 'shiftKey'
|
||||
>;
|
||||
}) => void;
|
||||
onStateChange?: (isCapturing: boolean) => void;
|
||||
focusRef?: MutableRefObject<KeyCaptureFocusInterface | null>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Key Capture is an invisible INPUT field that we set focus to when the user clicks inside of
|
||||
* the console. It's sole purpose is to capture what the user types, which is then pass along to be
|
||||
* displayed in a more UX friendly way
|
||||
*/
|
||||
export const KeyCapture = memo<KeyCaptureProps>(({ onCapture, focusRef, onStateChange }) => {
|
||||
// We don't need the actual value that was last input in this component, because
|
||||
// `setLastInput()` is used with a function that returns the typed character.
|
||||
// This state is used like this:
|
||||
// 1. User presses a keyboard key down
|
||||
// 2. We store the key that was pressed
|
||||
// 3. When the 'keyup' event is triggered, we call `onCapture()`
|
||||
// with all of the character that were entered
|
||||
// 4. We set the last input back to an empty string
|
||||
const getTestId = useTestIdGenerator(useDataTestSubj());
|
||||
const inputRef = useRef<HTMLInputElement | null>(null);
|
||||
const blurInputRef = useRef<HTMLInputElement | null>(null);
|
||||
|
||||
const [isCapturing, setIsCapturing] = useState(false);
|
||||
|
||||
const handleInputOnBlur = useCallback(() => {
|
||||
setIsCapturing(false);
|
||||
|
||||
if (onStateChange) {
|
||||
onStateChange(false);
|
||||
}
|
||||
}, [onStateChange]);
|
||||
|
||||
const handleInputOnFocus = useCallback<FormEventHandler>(
|
||||
(ev) => {
|
||||
setIsCapturing(true);
|
||||
|
||||
if (onStateChange) {
|
||||
onStateChange(true);
|
||||
}
|
||||
},
|
||||
[onStateChange]
|
||||
);
|
||||
|
||||
const handleInputOnPaste = useCallback<ClipboardEventHandler>(
|
||||
(ev) => {
|
||||
const value = ev.clipboardData.getData('text');
|
||||
ev.stopPropagation();
|
||||
|
||||
// hard-coded for use in onCapture and future keyboard functions
|
||||
const metaKey = {
|
||||
altKey: false,
|
||||
ctrlKey: false,
|
||||
key: 'Meta',
|
||||
keyCode: 91,
|
||||
metaKey: true,
|
||||
repeat: false,
|
||||
shiftKey: false,
|
||||
};
|
||||
|
||||
onCapture({
|
||||
value,
|
||||
eventDetails: metaKey,
|
||||
});
|
||||
},
|
||||
[onCapture]
|
||||
);
|
||||
|
||||
// 1. Determine if the key press is one that we need to store ex) letters, digits, values that we see
|
||||
// 2. If the user clicks a key we don't need to store as text, but we need to do logic with ex) backspace, delete, l/r arrows, we must call onCapture
|
||||
const handleOnKeyDown = useCallback<KeyboardEventHandler>(
|
||||
(ev) => {
|
||||
// checking to ensure that the key is not a control character
|
||||
const newValue = /^[\w\d]{2}/.test(ev.key) ? '' : ev.key;
|
||||
|
||||
// @ts-expect-error
|
||||
if (!isCapturing || ev._CONSOLE_IGNORE_KEY) {
|
||||
// @ts-expect-error
|
||||
if (ev._CONSOLE_IGNORE_KEY) {
|
||||
// @ts-expect-error
|
||||
ev._CONSOLE_IGNORE_KEY = false;
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
ev.stopPropagation();
|
||||
|
||||
// allows for clipboard events to be captured via onPaste event handler
|
||||
if (ev.metaKey || ev.ctrlKey) {
|
||||
return;
|
||||
}
|
||||
|
||||
const eventDetails = pick(ev, [
|
||||
'key',
|
||||
'altKey',
|
||||
'ctrlKey',
|
||||
'keyCode',
|
||||
'metaKey',
|
||||
'repeat',
|
||||
'shiftKey',
|
||||
]);
|
||||
|
||||
onCapture({
|
||||
value: newValue,
|
||||
eventDetails,
|
||||
});
|
||||
},
|
||||
[isCapturing, onCapture]
|
||||
);
|
||||
|
||||
const keyCaptureFocusMethods = useMemo<KeyCaptureFocusInterface>(() => {
|
||||
return {
|
||||
focus: (force: boolean = false) => {
|
||||
// If user selected text and `force` is not true, then don't focus (else they lose selection)
|
||||
if (!force && (window.getSelection()?.toString() ?? '').length > 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
inputRef.current?.focus();
|
||||
},
|
||||
|
||||
blur: () => {
|
||||
// only blur if the input has focus
|
||||
if (inputRef.current && document.activeElement === inputRef.current) {
|
||||
blurInputRef.current?.focus();
|
||||
}
|
||||
},
|
||||
};
|
||||
}, []);
|
||||
|
||||
if (focusRef) {
|
||||
focusRef.current = keyCaptureFocusMethods;
|
||||
}
|
||||
|
||||
return (
|
||||
<KeyCaptureContainer data-test-subj={getTestId('keyCapture')} aria-hidden="true" tabIndex={-1}>
|
||||
<input value="" ref={blurInputRef} tabIndex={-1} onChange={NOOP} />
|
||||
|
||||
<input
|
||||
className="invisible-input"
|
||||
data-test-subj={getTestId('keyCapture-input')}
|
||||
spellCheck="false"
|
||||
value=""
|
||||
tabIndex={-1}
|
||||
onKeyDown={handleOnKeyDown}
|
||||
onBlur={handleInputOnBlur}
|
||||
onFocus={handleInputOnFocus}
|
||||
onPaste={handleInputOnPaste}
|
||||
onChange={NOOP} // this just silences Jest output warnings
|
||||
ref={inputRef}
|
||||
/>
|
||||
</KeyCaptureContainer>
|
||||
);
|
||||
});
|
||||
KeyCapture.displayName = 'KeyCapture';
|
|
@ -0,0 +1,98 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Class that manages the command entered and how that is displayed to the left and right of the cursor
|
||||
*/
|
||||
export class EnteredInput {
|
||||
constructor(private leftOfCursorText: string, private rightOfCursorText: string) {}
|
||||
|
||||
private replaceSelection(selection: string, newValue: string) {
|
||||
const prevFullTextEntered = this.leftOfCursorText + this.rightOfCursorText;
|
||||
|
||||
this.leftOfCursorText =
|
||||
prevFullTextEntered.substring(0, prevFullTextEntered.indexOf(selection)) + newValue;
|
||||
|
||||
this.rightOfCursorText = prevFullTextEntered.substring(
|
||||
prevFullTextEntered.indexOf(selection) + selection.length
|
||||
);
|
||||
}
|
||||
|
||||
getLeftOfCursorText(): string {
|
||||
return this.leftOfCursorText;
|
||||
}
|
||||
|
||||
getRightOfCursorText(): string {
|
||||
return this.rightOfCursorText;
|
||||
}
|
||||
|
||||
getFullText(): string {
|
||||
return this.leftOfCursorText + this.rightOfCursorText;
|
||||
}
|
||||
|
||||
moveCursorTo(direction: 'left' | 'right' | 'end' | 'home') {
|
||||
switch (direction) {
|
||||
case 'end':
|
||||
this.leftOfCursorText = this.leftOfCursorText + this.rightOfCursorText;
|
||||
this.rightOfCursorText = '';
|
||||
break;
|
||||
|
||||
case 'home':
|
||||
this.rightOfCursorText = this.leftOfCursorText + this.rightOfCursorText;
|
||||
this.leftOfCursorText = '';
|
||||
break;
|
||||
|
||||
case 'left':
|
||||
if (this.leftOfCursorText.length) {
|
||||
// Add last character on the left, to the right side of the cursor
|
||||
this.rightOfCursorText =
|
||||
this.leftOfCursorText.charAt(this.leftOfCursorText.length - 1) + this.rightOfCursorText;
|
||||
|
||||
// Remove the last character from the left (it's now on the right side of cursor)
|
||||
this.leftOfCursorText = this.leftOfCursorText.substring(
|
||||
0,
|
||||
this.leftOfCursorText.length - 1
|
||||
);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'right':
|
||||
if (this.rightOfCursorText.length) {
|
||||
// MOve the first character from the Right side, to the left side of the cursor
|
||||
this.leftOfCursorText = this.leftOfCursorText + this.rightOfCursorText.charAt(0);
|
||||
|
||||
// Remove the first character from the Right side of the cursor (now on the left)
|
||||
this.rightOfCursorText = this.rightOfCursorText.substring(1);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
addValue(value: string, replaceSelection: string = '') {
|
||||
if (replaceSelection.length && value.length) {
|
||||
this.replaceSelection(replaceSelection, value);
|
||||
} else {
|
||||
this.leftOfCursorText += value;
|
||||
}
|
||||
}
|
||||
|
||||
deleteChar(replaceSelection: string = '') {
|
||||
if (replaceSelection) {
|
||||
this.replaceSelection(replaceSelection, '');
|
||||
} else if (this.rightOfCursorText) {
|
||||
this.rightOfCursorText = this.rightOfCursorText.substring(1);
|
||||
}
|
||||
}
|
||||
|
||||
backspaceChar(replaceSelection: string = '') {
|
||||
if (replaceSelection) {
|
||||
this.replaceSelection(replaceSelection, '');
|
||||
} else if (this.leftOfCursorText) {
|
||||
this.leftOfCursorText = this.leftOfCursorText.substring(0, this.leftOfCursorText.length - 1);
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue