Message Input: Fix focus delay in attachment modal (#6273)

This commit is contained in:
Alexander Zinchuk 2025-10-08 12:33:17 +02:00
parent d62cd024a9
commit 729ed3dd7d
11 changed files with 70 additions and 66 deletions

View File

@ -8,6 +8,7 @@ import {
import { requestMeasure } from '../../../lib/fasterdom/fasterdom';
import buildClassName from '../../../util/buildClassName';
import focusNoScroll from '../../../util/focusNoScroll';
import { MEMO_EMPTY_ARRAY } from '../../../util/memo';
import useInfiniteScroll from '../../../hooks/useInfiniteScroll';
@ -69,9 +70,6 @@ type OwnProps = {
onLoadMore?: () => void;
} & (SingleModeProps | MultipleModeProps);
// Focus slows down animation, also it breaks transition layout in Chrome
const FOCUS_DELAY_MS = 500;
const ITEM_CLASS_NAME = 'ItemPickerItem';
const ItemPicker = ({
@ -103,15 +101,9 @@ const ItemPicker = ({
useEffect(() => {
if (!isSearchable) return undefined;
const timeoutId = window.setTimeout(() => {
requestMeasure(() => {
inputRef.current?.focus();
});
}, FOCUS_DELAY_MS);
return () => {
window.clearTimeout(timeoutId);
};
requestMeasure(() => {
focusNoScroll(inputRef.current);
});
}, [isSearchable]);
const selectedValues = useMemo(() => {

View File

@ -12,6 +12,7 @@ import { getGroupStatus, getMainUsername, getUserStatus, isUserOnline } from '..
import { getPeerTypeKey, isApiPeerChat } from '../../../global/helpers/peers';
import { selectPeer, selectUserStatus } from '../../../global/selectors';
import buildClassName from '../../../util/buildClassName';
import focusNoScroll from '../../../util/focusNoScroll';
import { buildCollectionByKey } from '../../../util/iteratees';
import { MEMO_EMPTY_ARRAY } from '../../../util/memo';
@ -81,9 +82,6 @@ type OwnProps<CategoryType extends string> = {
onLoadMore?: () => void;
} & (SingleModeProps<CategoryType> | MultipleModeProps<CategoryType>);
// Focus slows down animation, also it breaks transition layout in Chrome
const FOCUS_DELAY_MS = 500;
const MAX_FULL_ITEMS = 10;
const ALWAYS_FULL_ITEMS_COUNT = 5;
@ -141,15 +139,9 @@ const PeerPicker = <CategoryType extends string = CustomPeerType>({
useEffect(() => {
if (!isSearchable) return undefined;
const timeoutId = window.setTimeout(() => {
requestMeasure(() => {
inputRef.current?.focus();
});
}, FOCUS_DELAY_MS);
return () => {
window.clearTimeout(timeoutId);
};
requestMeasure(() => {
focusNoScroll(inputRef.current);
});
}, [isSearchable]);
const lockedSelectedIdsSet = useMemo(() => new Set(lockedSelectedIds), [lockedSelectedIds]);

View File

@ -35,6 +35,7 @@ import {
} from '../../global/selectors';
import { ARE_CALLS_SUPPORTED, IS_APP } from '../../util/browser/windowEnvironment';
import { isUserId } from '../../util/entities/ids';
import focusNoScroll from '../../util/focusNoScroll';
import { useHotkeys } from '../../hooks/useHotkeys';
import useLastCallback from '../../hooks/useLastCallback';
@ -89,9 +90,6 @@ interface StateProps {
isAccountFrozen?: boolean;
}
// Chrome breaks layout when focusing input during transition
const SEARCH_FOCUS_DELAY_MS = 320;
const HeaderActions: FC<OwnProps & StateProps> = ({
chatId,
threadId,
@ -207,16 +205,13 @@ const HeaderActions: FC<OwnProps & StateProps> = ({
openMiddleSearch();
if (isMobile) {
// iOS requires synchronous focus on user event.
setFocusInSearchInput();
} else if (noAnimation) {
if (noAnimation) {
// The second RAF is necessary because Teact must update the state and render the async component
requestMeasure(() => {
requestNextMutation(setFocusInSearchInput);
});
} else {
setTimeout(setFocusInSearchInput, SEARCH_FOCUS_DELAY_MS);
setFocusInSearchInput();
}
});
@ -566,5 +561,7 @@ export default memo(withGlobal<OwnProps>(
function setFocusInSearchInput() {
const searchInput = document.querySelector<HTMLInputElement>('#MiddleSearch input');
searchInput?.focus();
if (searchInput) {
focusNoScroll(searchInput);
}
}

View File

@ -2,7 +2,6 @@ import type { ChangeEvent } from 'react';
import type { ElementRef, FC, TeactNode } from '../../../lib/teact/teact';
import type React from '../../../lib/teact/teact';
import {
getIsHeavyAnimating,
memo, useEffect, useLayoutEffect,
useRef, useState,
} from '../../../lib/teact/teact';
@ -45,8 +44,6 @@ import TextTimer from '../../ui/TextTimer';
import TextFormatter from './TextFormatter.async';
const CONTEXT_MENU_CLOSE_DELAY_MS = 100;
// Focus slows down animation, also it breaks transition layout in Chrome
const FOCUS_DELAY_MS = 350;
const TRANSITION_DURATION_FACTOR = 50;
const SCROLLER_CLASS = 'input-scroller';
@ -248,6 +245,10 @@ const MessageInput: FC<OwnProps & StateProps> = ({
useLayoutEffect(() => {
const html = isActive ? getHtml() : '';
if (!isActive && inputRef.current) {
inputRef.current.blur();
}
if (html !== inputRef.current!.innerHTML) {
inputRef.current!.innerHTML = html;
}
@ -270,11 +271,6 @@ const MessageInput: FC<OwnProps & StateProps> = ({
return;
}
if (getIsHeavyAnimating()) {
setTimeout(focusInput, FOCUS_DELAY_MS);
return;
}
focusEditableElement(inputRef.current);
});

View File

@ -35,6 +35,7 @@ import buildClassName from '../../../util/buildClassName';
import captureEscKeyListener from '../../../util/captureEscKeyListener';
import { getDayStartAt } from '../../../util/dates/dateFormat';
import focusEditableElement from '../../../util/focusEditableElement';
import focusNoScroll from '../../../util/focusNoScroll';
import { getSearchResultKey, parseSearchResultKey, type SearchResultKey } from '../../../util/keys/searchResultKey';
import { MEMO_EMPTY_ARRAY } from '../../../util/memo';
import { debounce, fastRaf } from '../../../util/schedulers';
@ -174,7 +175,7 @@ const MiddleSearch: FC<OwnProps & StateProps> = ({
const focusInput = useLastCallback(() => {
requestMeasure(() => {
inputRef.current?.focus();
focusNoScroll(inputRef.current);
});
});

View File

@ -4,10 +4,12 @@ import {
useRef, useState,
} from '../../lib/teact/teact';
import { requestMeasure } from '../../lib/fasterdom/fasterdom';
import { IS_TOUCH_ENV } from '../../util/browser/windowEnvironment';
import focusNoScroll from '../../util/focusNoScroll';
import { CardType, detectCardType } from '../common/helpers/detectCardType';
import { formatCardNumber } from '../middle/helpers/inputFormatters';
import useFocusAfterAnimation from '../../hooks/useFocusAfterAnimation';
import useOldLang from '../../hooks/useOldLang';
import InputText from '../ui/InputText';
@ -24,13 +26,22 @@ export type OwnProps = {
value: string;
error?: string;
onChange: (value: string) => void;
isActive?: boolean;
};
const CardInput: FC<OwnProps> = ({ value, error, onChange }) => {
const CardInput: FC<OwnProps> = ({ value, error, onChange, isActive }) => {
const lang = useOldLang();
const cardNumberRef = useRef<HTMLInputElement>();
useFocusAfterAnimation(cardNumberRef);
useEffect(() => {
if (!isActive || IS_TOUCH_ENV) {
return;
}
requestMeasure(() => {
focusNoScroll(cardNumberRef.current);
});
}, [isActive]);
const [cardType, setCardType] = useState<number>(CardType.Default);
useEffect(() => {

View File

@ -26,6 +26,7 @@ export type OwnProps = {
needZip?: boolean;
countryList: ApiCountry[];
dispatch: FormEditDispatch;
isActive?: boolean;
};
const PaymentInfo: FC<OwnProps> = ({
@ -36,6 +37,7 @@ const PaymentInfo: FC<OwnProps> = ({
needZip,
countryList,
dispatch,
isActive,
}) => {
const selectCountryRef = useRef<HTMLSelectElement>();
@ -88,6 +90,7 @@ const PaymentInfo: FC<OwnProps> = ({
onChange={handleCardNumberChange}
value={state.cardNumber}
error={formErrors.cardNumber && lang.withRegular(formErrors.cardNumber)}
isActive={isActive}
/>
{needCardholderName && (
<InputText

View File

@ -275,7 +275,7 @@ const PaymentModal: FC<OwnProps & StateProps> = ({
sendForm();
}, [sendForm]);
function renderModalContent(currentStep: PaymentStep) {
function renderModalContent(currentStep: PaymentStep, isActive?: boolean) {
switch (currentStep) {
case PaymentStep.Checkout:
return (
@ -328,6 +328,7 @@ const PaymentModal: FC<OwnProps & StateProps> = ({
needCountry={needCountry}
needZip={needZip}
countryList={countryList}
isActive={isActive}
/>
);
case PaymentStep.ShippingInfo:
@ -580,9 +581,11 @@ const PaymentModal: FC<OwnProps & StateProps> = ({
shouldCleanup
cleanupOnlyKey={PaymentStep.ConfirmPayment}
>
<div className="content custom-scroll">
{renderModalContent(step)}
</div>
{(isActive) => (
<div className="content custom-scroll">
{renderModalContent(step, isActive)}
</div>
)}
</Transition>
) : (
<div className="empty-content">

View File

@ -7,7 +7,10 @@ import {
import type { ApiCountry } from '../../api/types';
import type { FormEditDispatch, FormState } from '../../hooks/reducers/usePaymentReducer';
import useFocusAfterAnimation from '../../hooks/useFocusAfterAnimation';
import { requestMeasure } from '../../lib/fasterdom/fasterdom';
import { IS_TOUCH_ENV } from '../../util/browser/windowEnvironment';
import focusNoScroll from '../../util/focusNoScroll';
import useLang from '../../hooks/useLang';
import useOldLang from '../../hooks/useOldLang';
@ -50,7 +53,15 @@ const ShippingInfo: FC<OwnProps> = ({
const oldLang = useOldLang();
const lang = useLang();
useFocusAfterAnimation(inputRef);
useEffect(() => {
if (IS_TOUCH_ENV) {
return;
}
requestMeasure(() => {
focusNoScroll(inputRef.current);
});
}, [inputRef]);
const handleAddress1Change = useCallback((e) => {
dispatch({ type: 'changeAddress1', payload: e.target.value });

View File

@ -1,11 +1,10 @@
import type { ElementRef } from '../lib/teact/teact';
import { useEffect } from '../lib/teact/teact';
import { requestMutation } from '../lib/fasterdom/fasterdom';
import useAppLayout from './useAppLayout';
import { requestMeasure } from '../lib/fasterdom/fasterdom';
import { IS_TOUCH_ENV } from '../util/browser/windowEnvironment';
import focusNoScroll from '../util/focusNoScroll';
// Focus slows down animation, also it breaks transition layout in Chrome
const FOCUS_DELAY_MS = 500;
const MODAL_HIDE_DELAY_MS = 300;
export default function useInputFocusOnOpen(
@ -13,18 +12,12 @@ export default function useInputFocusOnOpen(
isOpen?: boolean,
onClose?: NoneToVoidFunction,
) {
const { isMobile } = useAppLayout();
useEffect(() => {
if (isOpen) {
if (!isMobile) {
setTimeout(() => {
requestMutation(() => {
if (inputRef.current?.isConnected) {
inputRef.current.focus();
}
});
}, FOCUS_DELAY_MS);
if (!IS_TOUCH_ENV && inputRef.current?.isConnected) {
requestMeasure(() => {
focusNoScroll(inputRef.current);
});
}
} else {
if (inputRef.current?.isConnected) {
@ -35,5 +28,5 @@ export default function useInputFocusOnOpen(
setTimeout(onClose, MODAL_HIDE_DELAY_MS);
}
}
}, [inputRef, isMobile, isOpen, onClose]);
}, [inputRef, isOpen, onClose]);
}

View File

@ -0,0 +1,5 @@
export default function focusNoScroll(element?: HTMLElement) {
if (!element) return;
element.focus({ preventScroll: true });
}