Message Input: Fix focus delay in attachment modal (#6273)
This commit is contained in:
parent
d62cd024a9
commit
729ed3dd7d
@ -8,6 +8,7 @@ import {
|
|||||||
|
|
||||||
import { requestMeasure } from '../../../lib/fasterdom/fasterdom';
|
import { requestMeasure } from '../../../lib/fasterdom/fasterdom';
|
||||||
import buildClassName from '../../../util/buildClassName';
|
import buildClassName from '../../../util/buildClassName';
|
||||||
|
import focusNoScroll from '../../../util/focusNoScroll';
|
||||||
import { MEMO_EMPTY_ARRAY } from '../../../util/memo';
|
import { MEMO_EMPTY_ARRAY } from '../../../util/memo';
|
||||||
|
|
||||||
import useInfiniteScroll from '../../../hooks/useInfiniteScroll';
|
import useInfiniteScroll from '../../../hooks/useInfiniteScroll';
|
||||||
@ -69,9 +70,6 @@ type OwnProps = {
|
|||||||
onLoadMore?: () => void;
|
onLoadMore?: () => void;
|
||||||
} & (SingleModeProps | MultipleModeProps);
|
} & (SingleModeProps | MultipleModeProps);
|
||||||
|
|
||||||
// Focus slows down animation, also it breaks transition layout in Chrome
|
|
||||||
const FOCUS_DELAY_MS = 500;
|
|
||||||
|
|
||||||
const ITEM_CLASS_NAME = 'ItemPickerItem';
|
const ITEM_CLASS_NAME = 'ItemPickerItem';
|
||||||
|
|
||||||
const ItemPicker = ({
|
const ItemPicker = ({
|
||||||
@ -103,15 +101,9 @@ const ItemPicker = ({
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!isSearchable) return undefined;
|
if (!isSearchable) return undefined;
|
||||||
const timeoutId = window.setTimeout(() => {
|
requestMeasure(() => {
|
||||||
requestMeasure(() => {
|
focusNoScroll(inputRef.current);
|
||||||
inputRef.current?.focus();
|
});
|
||||||
});
|
|
||||||
}, FOCUS_DELAY_MS);
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
window.clearTimeout(timeoutId);
|
|
||||||
};
|
|
||||||
}, [isSearchable]);
|
}, [isSearchable]);
|
||||||
|
|
||||||
const selectedValues = useMemo(() => {
|
const selectedValues = useMemo(() => {
|
||||||
|
|||||||
@ -12,6 +12,7 @@ import { getGroupStatus, getMainUsername, getUserStatus, isUserOnline } from '..
|
|||||||
import { getPeerTypeKey, isApiPeerChat } from '../../../global/helpers/peers';
|
import { getPeerTypeKey, isApiPeerChat } from '../../../global/helpers/peers';
|
||||||
import { selectPeer, selectUserStatus } from '../../../global/selectors';
|
import { selectPeer, selectUserStatus } from '../../../global/selectors';
|
||||||
import buildClassName from '../../../util/buildClassName';
|
import buildClassName from '../../../util/buildClassName';
|
||||||
|
import focusNoScroll from '../../../util/focusNoScroll';
|
||||||
import { buildCollectionByKey } from '../../../util/iteratees';
|
import { buildCollectionByKey } from '../../../util/iteratees';
|
||||||
import { MEMO_EMPTY_ARRAY } from '../../../util/memo';
|
import { MEMO_EMPTY_ARRAY } from '../../../util/memo';
|
||||||
|
|
||||||
@ -81,9 +82,6 @@ type OwnProps<CategoryType extends string> = {
|
|||||||
onLoadMore?: () => void;
|
onLoadMore?: () => void;
|
||||||
} & (SingleModeProps<CategoryType> | MultipleModeProps<CategoryType>);
|
} & (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 MAX_FULL_ITEMS = 10;
|
||||||
const ALWAYS_FULL_ITEMS_COUNT = 5;
|
const ALWAYS_FULL_ITEMS_COUNT = 5;
|
||||||
|
|
||||||
@ -141,15 +139,9 @@ const PeerPicker = <CategoryType extends string = CustomPeerType>({
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!isSearchable) return undefined;
|
if (!isSearchable) return undefined;
|
||||||
const timeoutId = window.setTimeout(() => {
|
requestMeasure(() => {
|
||||||
requestMeasure(() => {
|
focusNoScroll(inputRef.current);
|
||||||
inputRef.current?.focus();
|
});
|
||||||
});
|
|
||||||
}, FOCUS_DELAY_MS);
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
window.clearTimeout(timeoutId);
|
|
||||||
};
|
|
||||||
}, [isSearchable]);
|
}, [isSearchable]);
|
||||||
|
|
||||||
const lockedSelectedIdsSet = useMemo(() => new Set(lockedSelectedIds), [lockedSelectedIds]);
|
const lockedSelectedIdsSet = useMemo(() => new Set(lockedSelectedIds), [lockedSelectedIds]);
|
||||||
|
|||||||
@ -35,6 +35,7 @@ import {
|
|||||||
} from '../../global/selectors';
|
} from '../../global/selectors';
|
||||||
import { ARE_CALLS_SUPPORTED, IS_APP } from '../../util/browser/windowEnvironment';
|
import { ARE_CALLS_SUPPORTED, IS_APP } from '../../util/browser/windowEnvironment';
|
||||||
import { isUserId } from '../../util/entities/ids';
|
import { isUserId } from '../../util/entities/ids';
|
||||||
|
import focusNoScroll from '../../util/focusNoScroll';
|
||||||
|
|
||||||
import { useHotkeys } from '../../hooks/useHotkeys';
|
import { useHotkeys } from '../../hooks/useHotkeys';
|
||||||
import useLastCallback from '../../hooks/useLastCallback';
|
import useLastCallback from '../../hooks/useLastCallback';
|
||||||
@ -89,9 +90,6 @@ interface StateProps {
|
|||||||
isAccountFrozen?: boolean;
|
isAccountFrozen?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Chrome breaks layout when focusing input during transition
|
|
||||||
const SEARCH_FOCUS_DELAY_MS = 320;
|
|
||||||
|
|
||||||
const HeaderActions: FC<OwnProps & StateProps> = ({
|
const HeaderActions: FC<OwnProps & StateProps> = ({
|
||||||
chatId,
|
chatId,
|
||||||
threadId,
|
threadId,
|
||||||
@ -207,16 +205,13 @@ const HeaderActions: FC<OwnProps & StateProps> = ({
|
|||||||
|
|
||||||
openMiddleSearch();
|
openMiddleSearch();
|
||||||
|
|
||||||
if (isMobile) {
|
if (noAnimation) {
|
||||||
// iOS requires synchronous focus on user event.
|
|
||||||
setFocusInSearchInput();
|
|
||||||
} else if (noAnimation) {
|
|
||||||
// The second RAF is necessary because Teact must update the state and render the async component
|
// The second RAF is necessary because Teact must update the state and render the async component
|
||||||
requestMeasure(() => {
|
requestMeasure(() => {
|
||||||
requestNextMutation(setFocusInSearchInput);
|
requestNextMutation(setFocusInSearchInput);
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
setTimeout(setFocusInSearchInput, SEARCH_FOCUS_DELAY_MS);
|
setFocusInSearchInput();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -566,5 +561,7 @@ export default memo(withGlobal<OwnProps>(
|
|||||||
|
|
||||||
function setFocusInSearchInput() {
|
function setFocusInSearchInput() {
|
||||||
const searchInput = document.querySelector<HTMLInputElement>('#MiddleSearch input');
|
const searchInput = document.querySelector<HTMLInputElement>('#MiddleSearch input');
|
||||||
searchInput?.focus();
|
if (searchInput) {
|
||||||
|
focusNoScroll(searchInput);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,7 +2,6 @@ import type { ChangeEvent } from 'react';
|
|||||||
import type { ElementRef, FC, TeactNode } from '../../../lib/teact/teact';
|
import type { ElementRef, FC, TeactNode } from '../../../lib/teact/teact';
|
||||||
import type React from '../../../lib/teact/teact';
|
import type React from '../../../lib/teact/teact';
|
||||||
import {
|
import {
|
||||||
getIsHeavyAnimating,
|
|
||||||
memo, useEffect, useLayoutEffect,
|
memo, useEffect, useLayoutEffect,
|
||||||
useRef, useState,
|
useRef, useState,
|
||||||
} from '../../../lib/teact/teact';
|
} from '../../../lib/teact/teact';
|
||||||
@ -45,8 +44,6 @@ import TextTimer from '../../ui/TextTimer';
|
|||||||
import TextFormatter from './TextFormatter.async';
|
import TextFormatter from './TextFormatter.async';
|
||||||
|
|
||||||
const CONTEXT_MENU_CLOSE_DELAY_MS = 100;
|
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 TRANSITION_DURATION_FACTOR = 50;
|
||||||
|
|
||||||
const SCROLLER_CLASS = 'input-scroller';
|
const SCROLLER_CLASS = 'input-scroller';
|
||||||
@ -248,6 +245,10 @@ const MessageInput: FC<OwnProps & StateProps> = ({
|
|||||||
useLayoutEffect(() => {
|
useLayoutEffect(() => {
|
||||||
const html = isActive ? getHtml() : '';
|
const html = isActive ? getHtml() : '';
|
||||||
|
|
||||||
|
if (!isActive && inputRef.current) {
|
||||||
|
inputRef.current.blur();
|
||||||
|
}
|
||||||
|
|
||||||
if (html !== inputRef.current!.innerHTML) {
|
if (html !== inputRef.current!.innerHTML) {
|
||||||
inputRef.current!.innerHTML = html;
|
inputRef.current!.innerHTML = html;
|
||||||
}
|
}
|
||||||
@ -270,11 +271,6 @@ const MessageInput: FC<OwnProps & StateProps> = ({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (getIsHeavyAnimating()) {
|
|
||||||
setTimeout(focusInput, FOCUS_DELAY_MS);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
focusEditableElement(inputRef.current);
|
focusEditableElement(inputRef.current);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -35,6 +35,7 @@ import buildClassName from '../../../util/buildClassName';
|
|||||||
import captureEscKeyListener from '../../../util/captureEscKeyListener';
|
import captureEscKeyListener from '../../../util/captureEscKeyListener';
|
||||||
import { getDayStartAt } from '../../../util/dates/dateFormat';
|
import { getDayStartAt } from '../../../util/dates/dateFormat';
|
||||||
import focusEditableElement from '../../../util/focusEditableElement';
|
import focusEditableElement from '../../../util/focusEditableElement';
|
||||||
|
import focusNoScroll from '../../../util/focusNoScroll';
|
||||||
import { getSearchResultKey, parseSearchResultKey, type SearchResultKey } from '../../../util/keys/searchResultKey';
|
import { getSearchResultKey, parseSearchResultKey, type SearchResultKey } from '../../../util/keys/searchResultKey';
|
||||||
import { MEMO_EMPTY_ARRAY } from '../../../util/memo';
|
import { MEMO_EMPTY_ARRAY } from '../../../util/memo';
|
||||||
import { debounce, fastRaf } from '../../../util/schedulers';
|
import { debounce, fastRaf } from '../../../util/schedulers';
|
||||||
@ -174,7 +175,7 @@ const MiddleSearch: FC<OwnProps & StateProps> = ({
|
|||||||
|
|
||||||
const focusInput = useLastCallback(() => {
|
const focusInput = useLastCallback(() => {
|
||||||
requestMeasure(() => {
|
requestMeasure(() => {
|
||||||
inputRef.current?.focus();
|
focusNoScroll(inputRef.current);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -4,10 +4,12 @@ import {
|
|||||||
useRef, useState,
|
useRef, useState,
|
||||||
} from '../../lib/teact/teact';
|
} 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 { CardType, detectCardType } from '../common/helpers/detectCardType';
|
||||||
import { formatCardNumber } from '../middle/helpers/inputFormatters';
|
import { formatCardNumber } from '../middle/helpers/inputFormatters';
|
||||||
|
|
||||||
import useFocusAfterAnimation from '../../hooks/useFocusAfterAnimation';
|
|
||||||
import useOldLang from '../../hooks/useOldLang';
|
import useOldLang from '../../hooks/useOldLang';
|
||||||
|
|
||||||
import InputText from '../ui/InputText';
|
import InputText from '../ui/InputText';
|
||||||
@ -24,13 +26,22 @@ export type OwnProps = {
|
|||||||
value: string;
|
value: string;
|
||||||
error?: string;
|
error?: string;
|
||||||
onChange: (value: string) => void;
|
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 lang = useOldLang();
|
||||||
const cardNumberRef = useRef<HTMLInputElement>();
|
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);
|
const [cardType, setCardType] = useState<number>(CardType.Default);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|||||||
@ -26,6 +26,7 @@ export type OwnProps = {
|
|||||||
needZip?: boolean;
|
needZip?: boolean;
|
||||||
countryList: ApiCountry[];
|
countryList: ApiCountry[];
|
||||||
dispatch: FormEditDispatch;
|
dispatch: FormEditDispatch;
|
||||||
|
isActive?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
const PaymentInfo: FC<OwnProps> = ({
|
const PaymentInfo: FC<OwnProps> = ({
|
||||||
@ -36,6 +37,7 @@ const PaymentInfo: FC<OwnProps> = ({
|
|||||||
needZip,
|
needZip,
|
||||||
countryList,
|
countryList,
|
||||||
dispatch,
|
dispatch,
|
||||||
|
isActive,
|
||||||
}) => {
|
}) => {
|
||||||
const selectCountryRef = useRef<HTMLSelectElement>();
|
const selectCountryRef = useRef<HTMLSelectElement>();
|
||||||
|
|
||||||
@ -88,6 +90,7 @@ const PaymentInfo: FC<OwnProps> = ({
|
|||||||
onChange={handleCardNumberChange}
|
onChange={handleCardNumberChange}
|
||||||
value={state.cardNumber}
|
value={state.cardNumber}
|
||||||
error={formErrors.cardNumber && lang.withRegular(formErrors.cardNumber)}
|
error={formErrors.cardNumber && lang.withRegular(formErrors.cardNumber)}
|
||||||
|
isActive={isActive}
|
||||||
/>
|
/>
|
||||||
{needCardholderName && (
|
{needCardholderName && (
|
||||||
<InputText
|
<InputText
|
||||||
|
|||||||
@ -275,7 +275,7 @@ const PaymentModal: FC<OwnProps & StateProps> = ({
|
|||||||
sendForm();
|
sendForm();
|
||||||
}, [sendForm]);
|
}, [sendForm]);
|
||||||
|
|
||||||
function renderModalContent(currentStep: PaymentStep) {
|
function renderModalContent(currentStep: PaymentStep, isActive?: boolean) {
|
||||||
switch (currentStep) {
|
switch (currentStep) {
|
||||||
case PaymentStep.Checkout:
|
case PaymentStep.Checkout:
|
||||||
return (
|
return (
|
||||||
@ -328,6 +328,7 @@ const PaymentModal: FC<OwnProps & StateProps> = ({
|
|||||||
needCountry={needCountry}
|
needCountry={needCountry}
|
||||||
needZip={needZip}
|
needZip={needZip}
|
||||||
countryList={countryList}
|
countryList={countryList}
|
||||||
|
isActive={isActive}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
case PaymentStep.ShippingInfo:
|
case PaymentStep.ShippingInfo:
|
||||||
@ -580,9 +581,11 @@ const PaymentModal: FC<OwnProps & StateProps> = ({
|
|||||||
shouldCleanup
|
shouldCleanup
|
||||||
cleanupOnlyKey={PaymentStep.ConfirmPayment}
|
cleanupOnlyKey={PaymentStep.ConfirmPayment}
|
||||||
>
|
>
|
||||||
<div className="content custom-scroll">
|
{(isActive) => (
|
||||||
{renderModalContent(step)}
|
<div className="content custom-scroll">
|
||||||
</div>
|
{renderModalContent(step, isActive)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</Transition>
|
</Transition>
|
||||||
) : (
|
) : (
|
||||||
<div className="empty-content">
|
<div className="empty-content">
|
||||||
|
|||||||
@ -7,7 +7,10 @@ import {
|
|||||||
import type { ApiCountry } from '../../api/types';
|
import type { ApiCountry } from '../../api/types';
|
||||||
import type { FormEditDispatch, FormState } from '../../hooks/reducers/usePaymentReducer';
|
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 useLang from '../../hooks/useLang';
|
||||||
import useOldLang from '../../hooks/useOldLang';
|
import useOldLang from '../../hooks/useOldLang';
|
||||||
|
|
||||||
@ -50,7 +53,15 @@ const ShippingInfo: FC<OwnProps> = ({
|
|||||||
const oldLang = useOldLang();
|
const oldLang = useOldLang();
|
||||||
const lang = useLang();
|
const lang = useLang();
|
||||||
|
|
||||||
useFocusAfterAnimation(inputRef);
|
useEffect(() => {
|
||||||
|
if (IS_TOUCH_ENV) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
requestMeasure(() => {
|
||||||
|
focusNoScroll(inputRef.current);
|
||||||
|
});
|
||||||
|
}, [inputRef]);
|
||||||
|
|
||||||
const handleAddress1Change = useCallback((e) => {
|
const handleAddress1Change = useCallback((e) => {
|
||||||
dispatch({ type: 'changeAddress1', payload: e.target.value });
|
dispatch({ type: 'changeAddress1', payload: e.target.value });
|
||||||
|
|||||||
@ -1,11 +1,10 @@
|
|||||||
import type { ElementRef } from '../lib/teact/teact';
|
import type { ElementRef } from '../lib/teact/teact';
|
||||||
import { useEffect } from '../lib/teact/teact';
|
import { useEffect } from '../lib/teact/teact';
|
||||||
|
|
||||||
import { requestMutation } from '../lib/fasterdom/fasterdom';
|
import { requestMeasure } from '../lib/fasterdom/fasterdom';
|
||||||
import useAppLayout from './useAppLayout';
|
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;
|
const MODAL_HIDE_DELAY_MS = 300;
|
||||||
|
|
||||||
export default function useInputFocusOnOpen(
|
export default function useInputFocusOnOpen(
|
||||||
@ -13,18 +12,12 @@ export default function useInputFocusOnOpen(
|
|||||||
isOpen?: boolean,
|
isOpen?: boolean,
|
||||||
onClose?: NoneToVoidFunction,
|
onClose?: NoneToVoidFunction,
|
||||||
) {
|
) {
|
||||||
const { isMobile } = useAppLayout();
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isOpen) {
|
if (isOpen) {
|
||||||
if (!isMobile) {
|
if (!IS_TOUCH_ENV && inputRef.current?.isConnected) {
|
||||||
setTimeout(() => {
|
requestMeasure(() => {
|
||||||
requestMutation(() => {
|
focusNoScroll(inputRef.current);
|
||||||
if (inputRef.current?.isConnected) {
|
});
|
||||||
inputRef.current.focus();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}, FOCUS_DELAY_MS);
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (inputRef.current?.isConnected) {
|
if (inputRef.current?.isConnected) {
|
||||||
@ -35,5 +28,5 @@ export default function useInputFocusOnOpen(
|
|||||||
setTimeout(onClose, MODAL_HIDE_DELAY_MS);
|
setTimeout(onClose, MODAL_HIDE_DELAY_MS);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [inputRef, isMobile, isOpen, onClose]);
|
}, [inputRef, isOpen, onClose]);
|
||||||
}
|
}
|
||||||
|
|||||||
5
src/util/focusNoScroll.ts
Normal file
5
src/util/focusNoScroll.ts
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
export default function focusNoScroll(element?: HTMLElement) {
|
||||||
|
if (!element) return;
|
||||||
|
|
||||||
|
element.focus({ preventScroll: true });
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user