Message Input: Allow typing even when not focused (#2135)

This commit is contained in:
Alexander Zinchuk 2022-11-16 16:16:30 +04:00
parent 6fb1f87b17
commit 2b37128066
5 changed files with 97 additions and 7 deletions

View File

@ -31,6 +31,7 @@ import { IS_SINGLE_COLUMN_LAYOUT, IS_TOUCH_ENV } from '../../util/environment';
import { ANIMATION_END_DELAY } from '../../config';
import { MEDIA_VIEWER_MEDIA_QUERY } from '../common/helpers/mediaDimensions';
import windowSize from '../../util/windowSize';
import { disableDirectTextInput, enableDirectTextInput } from '../../util/directInputManager';
import { animateClosing, animateOpening } from './helpers/ghostAnimation';
import { renderMessageText } from '../common/helpers/renderMessageText';
@ -144,6 +145,16 @@ const MediaViewer: FC<StateProps> = ({
animationKey.current = selectedMediaIndex;
}
useEffect(() => {
if (!isOpen) {
return undefined;
}
disableDirectTextInput();
return enableDirectTextInput;
}, [isOpen]);
useEffect(() => {
if (isVisible) {
exitPictureInPictureIfNeeded();

View File

@ -1237,6 +1237,7 @@ const Composer: FC<OwnProps & StateProps> = ({
)}
<MessageInput
id="message-input-text"
editableInputId={EDITABLE_INPUT_ID}
chatId={chatId}
threadId={threadId}
html={!attachments.length ? html : ''}
@ -1247,6 +1248,7 @@ const Composer: FC<OwnProps & StateProps> = ({
}
forcedPlaceholder={inlineBotHelp}
canAutoFocus={isReady && !attachments.length}
noFocusInterception={attachments.length > 0}
shouldSuppressFocus={IS_SINGLE_COLUMN_LAYOUT && isSymbolMenuOpen}
shouldSuppressTextFormatter={isEmojiTooltipOpen || isMentionTooltipOpen || isInlineBotTooltipOpen}
onUpdate={setHtml}

View File

@ -8,7 +8,7 @@ import { getActions, withGlobal } from '../../../global';
import type { IAnchorPosition, ISettings } from '../../../types';
import { EDITABLE_INPUT_ID } from '../../../config';
import { selectReplyingToId } from '../../../global/selectors';
import { selectIsInSelectMode, selectReplyingToId } from '../../../global/selectors';
import { debounce } from '../../../util/schedulers';
import focusEditableElement from '../../../util/focusEditableElement';
import buildClassName from '../../../util/buildClassName';
@ -16,6 +16,7 @@ import {
IS_ANDROID, IS_EMOJI_SUPPORTED, IS_IOS, IS_SINGLE_COLUMN_LAYOUT, IS_TOUCH_ENV,
} from '../../../util/environment';
import captureKeyboardListeners from '../../../util/captureKeyboardListeners';
import { getIsDirectTextInputDisabled } from '../../../util/directInputManager';
import useLayoutEffectWithPrevDeps from '../../../hooks/useLayoutEffectWithPrevDeps';
import useFlag from '../../../hooks/useFlag';
import { isHeavyAnimating } from '../../../hooks/useHeavyAnimationCheck';
@ -40,6 +41,7 @@ type OwnProps = {
html: string;
placeholder: string;
forcedPlaceholder?: string;
noFocusInterception?: boolean;
canAutoFocus: boolean;
shouldSuppressFocus?: boolean;
shouldSuppressTextFormatter?: boolean;
@ -51,7 +53,7 @@ type OwnProps = {
type StateProps = {
replyingToId?: number;
noTabCapture?: boolean;
isSelectModeActive?: boolean;
messageSendKeyCombo?: ISettings['messageSendKeyCombo'];
};
@ -62,6 +64,7 @@ const SELECTION_RECALCULATE_DELAY_MS = 260;
const TEXT_FORMATTER_SAFE_AREA_PX = 90;
// For some reason Safari inserts `<br>` after user removes text from input
const SAFARI_BR = '<br>';
const IGNORE_KEYS = ['Enter', 'PageUp', 'PageDown', 'Meta', 'Alt', 'Ctrl', 'ArrowDown', 'ArrowUp'];
function clearSelection() {
const selection = window.getSelection();
@ -86,10 +89,11 @@ const MessageInput: FC<OwnProps & StateProps> = ({
placeholder,
forcedPlaceholder,
canAutoFocus,
noFocusInterception,
shouldSuppressFocus,
shouldSuppressTextFormatter,
replyingToId,
noTabCapture,
isSelectModeActive,
messageSendKeyCombo,
onUpdate,
onSuppressedFocus,
@ -363,19 +367,68 @@ const MessageInput: FC<OwnProps & StateProps> = ({
}, [chatId, focusInput, replyingToId, canAutoFocus]);
useEffect(() => {
if (noTabCapture) {
if (
!chatId
|| editableInputId !== EDITABLE_INPUT_ID
|| noFocusInterception
|| (IS_TOUCH_ENV && IS_SINGLE_COLUMN_LAYOUT)
|| isSelectModeActive
) {
return undefined;
}
const handleDocumentKeyDown = (e: KeyboardEvent) => {
if (getIsDirectTextInputDisabled()) {
return;
}
const { key } = e;
const target = e.target as HTMLElement | undefined;
if (!target || IGNORE_KEYS.includes(key)) {
return;
}
const input = inputRef.current!;
const isSelectionCollapsed = document.getSelection()?.isCollapsed;
if (
((key.startsWith('Arrow') || (e.shiftKey && key === 'Shift')) && !isSelectionCollapsed)
|| (e.code === 'KeyC' && (e.ctrlKey || e.metaKey) && target.tagName !== 'INPUT')
) {
return;
}
if (
input
&& target !== input
&& target.tagName !== 'INPUT'
&& !target.isContentEditable
) {
focusEditableElement(input, true, true);
const newEvent = new KeyboardEvent(e.type, e as any);
input.dispatchEvent(newEvent);
}
};
document.addEventListener('keydown', handleDocumentKeyDown, true);
return () => {
document.removeEventListener('keydown', handleDocumentKeyDown, true);
};
}, [chatId, editableInputId, isSelectModeActive, noFocusInterception]);
useEffect(() => {
const captureFirstTab = debounce((e: KeyboardEvent) => {
if (e.key === 'Tab') {
if (e.key === 'Tab' && !getIsDirectTextInputDisabled()) {
e.preventDefault();
requestAnimationFrame(focusInput);
}
}, TAB_INDEX_PRIORITY_TIMEOUT, true, false);
return captureKeyboardListeners({ onTab: captureFirstTab });
}, [focusInput, noTabCapture]);
}, [focusInput]);
useEffect(() => {
const input = inputRef.current!;
@ -444,7 +497,7 @@ export default memo(withGlobal<OwnProps>(
return {
messageSendKeyCombo,
replyingToId: chatId && threadId ? selectReplyingToId(global, chatId, threadId) : undefined,
noTabCapture: global.pollModal.isOpen || global.payment.isPaymentModalOpen,
isSelectModeActive: selectIsInSelectMode(global),
};
},
)(MessageInput));

View File

@ -7,6 +7,7 @@ import type { TextPart } from '../../types';
import captureKeyboardListeners from '../../util/captureKeyboardListeners';
import trapFocus from '../../util/trapFocus';
import buildClassName from '../../util/buildClassName';
import { enableDirectTextInput, disableDirectTextInput } from '../../util/directInputManager';
import { dispatchHeavyAnimationEvent } from '../../hooks/useHeavyAnimationCheck';
import useShowTransition from '../../hooks/useShowTransition';
import useEffectWithPrevDeps from '../../hooks/useEffectWithPrevDeps';
@ -63,6 +64,16 @@ const Modal: FC<OwnProps & StateProps> = ({
// eslint-disable-next-line no-null/no-null
const modalRef = useRef<HTMLDivElement>(null);
useEffect(() => {
if (!isOpen) {
return undefined;
}
disableDirectTextInput();
return enableDirectTextInput;
}, [isOpen]);
useEffect(() => (isOpen
? captureKeyboardListeners({ onEsc: onClose, onEnter })
: undefined), [isOpen, onClose, onEnter]);

View File

@ -0,0 +1,13 @@
let counter = 0;
export function disableDirectTextInput() {
counter += 1;
}
export function enableDirectTextInput() {
counter -= 1;
}
export function getIsDirectTextInputDisabled() {
return counter > 0;
}