Hello!
I’m trying to set focus to a textarea in a modal.
Following Docs I’m using the following TSX component to focus on an input.
On iOS, however, the keyboard does not appear when the modal opens, even though the input is focused.
Same with IonTextArea
import React, { useRef } from 'react';
import { IonButton, IonContent, IonInput, IonModal } from '@ionic/react';
interface ComponentProps {
isModalOpen: boolean;
closeModal: () => void;
}
const StringTestFocusIonic: React.FC<ComponentProps> = ({ isModalOpen, closeModal }) => {
const input = useRef<HTMLIonInputElement>(null);
const onDidPresent = () => {
input.current?.setFocus();
};
return (
<IonModal
isOpen={isModalOpen}
onDidPresent={onDidPresent}
onDidDismiss={closeModal}
>
<IonContent>
<IonInput ref={input}></IonInput>
<IonButton onClick={closeModal}>Close</IonButton>
</IonContent>
</IonModal>
);
};
export default StringTestFocusIonic;
This is the code I use to autofocus an input element, and it works on iOS:
useAutofocus.ts
import React from 'react';
type AutofocusableElement = HTMLIonInputElement | HTMLIonTextareaElement;
type UseAutofocusOptions = {
condition?: boolean | (() => boolean);
delay?: number;
};
// The default is quite short because modal components should load quickly.
const defaultDelay = 10;
/**
* Returns a ref that can be used to enable Ionic components in modals to be autofocused.
*/
export const useAutofocus = <
T extends AutofocusableElement = AutofocusableElement,
>(
options: UseAutofocusOptions = {},
) => {
const { condition = true, delay = defaultDelay } = options;
const ref = React.useRef<T | null>(null);
React.useEffect(() => {
const shouldFocus =
typeof condition === 'function' ? condition() : condition;
if (shouldFocus) {
const timeoutId = setTimeout(() => {
void ref.current?.setFocus();
}, delay);
return () => clearTimeout(timeoutId);
}
}, [condition, delay]);
return ref;
};
This hook is called inside the hook that sets up my input form using tanstack-form. Here’s the relevant part for scrolling:
const myInputRef = useAutofocus<HTMLIonInputElement>();
const focusMyInput = () => {
const input = myInputRef.current;
if (input === null) {
return;
}
requestAnimationFrame(() => {
const current = myInputRef.current;
if (current === null) {
return;
}
// requestAnimationFrame ensures toggles finish rendering before we grab
// the native input; preventScroll keeps the button the user clicked in view.
void current.getInputElement().then((nativeInput) => {
nativeInput.focus({ preventScroll: true });
});
});
};
IThank you, ptmkenny! Very generous of you to share your code. Unfortunately, I haven’t gotten it working yet. It works in my browsers on Android and Linux, but when opening my modal in Safari, the keyboard doesn’t appear.
interface ComponentProps {
closeModal: () => void;
isModalOpen: boolean;
}
// Autofocus hook for Ionic inputs/textareas
type AutofocusableElement = HTMLIonInputElement | HTMLIonTextareaElement;
type UseAutofocusOptions = { condition?: boolean | (() => boolean); delay?: number };
const defaultDelay = 10;
export const useAutofocus = <T extends AutofocusableElement = AutofocusableElement>(
options: UseAutofocusOptions = {},
) => {
const { condition = true, delay = defaultDelay } = options;
const ref = React.useRef<T | null>(null);
React.useEffect(() => {
const shouldFocus = typeof condition === "function" ? condition() : condition;
if (shouldFocus) {
const timeoutId = setTimeout(() => {
void ref.current?.setFocus();
}, delay);
return () => clearTimeout(timeoutId);
}
}, [condition, delay]);
return ref;
};
const StringTest1: React.FC<ComponentProps> = ({ isModalOpen, closeModal }) => {
const { allUserProfiles, updateUserProfiles, userId } = useDatabaseContext();
const userProfileItem = allUserProfiles.find(
(user) => user.user_id === userId
);
const dismissModal = async () => {
await updateUserProfiles({
user_id: userId,
changes: { greatest_achievements: localData },
});
closeModal();
};
const [localData, setLocalData] = useState<string>(
userProfileItem?.greatest_achievements ?? ""
);
useEffect(() => {
setLocalData(userProfileItem?.greatest_achievements ?? "");
}, [userProfileItem?.greatest_achievements]);
const handleInput = (e: CustomEvent) => {
setLocalData(e.detail.value ?? "");
};
// Create a ref that auto-focuses when the modal is open
const textareaRef = useAutofocus<HTMLIonTextareaElement>({ condition: isModalOpen });
// Example usage: focus the native textarea element with preventScroll
const focusTextarea = () => {
const el = textareaRef.current;
if (el === null) return;
requestAnimationFrame(() => {
const current = textareaRef.current;
if (current === null) return;
void current.getInputElement().then((nativeInput) => {
nativeInput.focus({ preventScroll: true });
});
});
};
useEffect(() => {
if (isModalOpen) {
focusTextarea();
}
}, [isModalOpen]);
return (
<IonModal
breakpoints={[0, 1]}
initialBreakpoint={1}
isOpen={isModalOpen}
onDidDismiss={dismissModal}
>
<HeaderTop onClickClose={closeModal} title1="test 1"></HeaderTop>
<IonContent fullscreen={true} scrollY={true}>
<IonTextarea
autoGrow
ref={textareaRef}
value={localData}
onIonInput={handleInput}
placeholder="Test here..."
/>
</IonContent>
</IonModal>
);
};
export default StringTest1;