I have the following input component that I'm reusing all over the place. It works absolutely great on desktop, but on mobile, I can't seem to get it to focus on press. I'm not sure what I'm doing wrong and everything I search is showing React Native which I'm not using. I saw some suggestions around adding a button, but that just seems weird to me. Should I add an onClick handler to the input?
import React, { useRef } from 'react'
const Input = ({
label,
type,
name,
placeholder,
autoComplete,
required,
className,
disabled,
onChange,
}) => {
const inputRef = useRef(null)
const handleFocus = () => {
if (inputRef.current) {
inputRef.current.focus()
}
}
return (
<div className={`${!!label ? 'form-floating' : ''}`}>
<input
ref={inputRef}
className={`form-control ${className ?? ''}`}
type={type}
name={name}
id={name}
placeholder={placeholder}
required={required}
autoComplete={autoComplete}
disabled={disabled}
onChange={(e) => onChange({ payload: e })}
onClick={handleFocus}
/>
{!!label && <label htmlFor={name}>{placeholder}</label>}
</div>
)
}
export default Input
I expected the cursor to pop up and the keyboard of the device to popup (on mobile), but nothing happened. I know I can set autoFocus, but that doesn't help since it's a re-useable component and can't have autoFocus on multiple instances on a page.
EDIT: Updating to use onClick instead of onFocus. Still does not work. The Inputs are in a modal if that makes a difference. Here's the Login form code
import { useEffect, useState, useReducer } from 'react'
import { useLoginMutation, useGoogleLoginMutation } from './authApiSlice'
import usePersist from '../../hooks/usePersist'
import { useNavigate } from 'react-router-dom'
import { useGoogleLogin } from '@react-oauth/google'
import formReducer from '../../hooks/useReducer'
import Input from '../../components/Input'
const PWD_REGEX =
/^(?=.*?[A-Z])(?=.*?[a-z])(?=.*?[0-9])(?=.*?[#?!@$%^&*-]).{8,15}$/
const USER_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
const Login = () => {
// Login hook
const [login, { isLoading }] = useLoginMutation()
const [googleLogin, { isLoading: isGoogleLoading }] = useGoogleLoginMutation()
const navigate = useNavigate()
const [state, setState] = useReducer(formReducer, {})
const [persist, setPersist] = usePersist()
const [errMsg, setErrMsg] = useState('')
useEffect(() => {
setState({
payload: {
target: {
name: 'validUsername',
value: USER_REGEX.test(state.username),
},
},
})
}, [state.username])
useEffect(() => {
setState({
payload: {
target: {
name: 'validPassword',
value: PWD_REGEX.test(state.password),
},
},
})
}, [state.password])
// Login function
const onLoginClicked = async () => {
if (!state.validUsername) {
setState({ payload: { target: { name: 'validUClass', value: false } } })
} else if (!state.validPassword) {
setState({ payload: { target: { name: 'validPClass', value: false } } })
} else {
try {
await login({ username: state.username, password: state.password })
navigate('/')
} catch (err) {
if (!err.status) {
setErrMsg('No Server Response')
} else {
setErrMsg(err.message)
}
}
}
}
const onGoogleLoginClicked = useGoogleLogin({
onSuccess: async ({ code }) => {
try {
await googleLogin({ code })
navigate('/')
} catch (err) {
console.log(err.message)
}
},
flow: 'auth-code',
})
const content = (
<form>
<div className='position-relative mb-3'>
<Input
type='email'
className={`${
state.validUClass && state.validUClass === false ? 'is-invalid' : ''
}`}
name='username'
label={true}
placeholder='Email Address'
autoComplete='username'
onChange={setState}
/>
<div id='usernameFeedback' className='invalid-feedback'>
Username must be a valid email address.
</div>
</div>
<div className='input-group mb-3 has-validation'>
<Input
type={state.showPassword ? 'text' : 'password'}
className={`${
state.validPClass && state.validPClass === false ? 'is-invalid' : ''
}`}
name='password'
placeholder='Password'
label={true}
autoComplete='password'
onChange={setState}
aria-describedby='eye-addon'
/>
<div id='passwordFeedback' className='invalid-feedback'>
Password must contain 1 uppercase, lowercase, numeric, and special
character
</div>
<span
className='input-group-text bi bi-eye>'
id='eye-addon'
onClick={() =>
setState({
payload: {
target: { name: 'showPassword', value: !state.showPassword },
},
})
}
>
<i
className={state.showPassword ? 'bi bi-eye-slash' : 'bi bi-eye'}
></i>
</span>
</div>
<div className='pb-2'>
<input
type='checkbox'
className='form-check-input'
id='persist'
onChange={() => setPersist((prev) => !prev)}
checked={persist}
/>
<label htmlFor='persist' className='ps-2'>
Trust This Device
</label>
</div>
<button
className='w-100 mb-2 btn btn-lg rounded-3 btn-primary'
type='submit'
disabled={state?.submitting}
onClick={(e) => {
e.preventDefault()
onLoginClicked()
}}
>
{(!isLoading || !isGoogleLoading) && !state.submitting
? 'Submit'
: 'Submitting'}
</button>
{errMsg && <div className='register-error'>{errMsg}</div>}
<div className='text-body-secondary text-center'>
By clicking Submit, you agree to the terms of use.
</div>
<hr className='my-4' />
<h2 className='fs-5 fw-bold mb-3 text-center'>Or use a third-party</h2>
<button
className='w-100 py-2 mb-2 btn btn-outline-primary rounded-3'
type='button'
onClick={onGoogleLoginClicked}
>
<i className='bi bi-google'></i> Login with Google
</button>
<button
className='w-100 py-2 mb-2 btn btn-outline-primary rounded-3'
type='button'
>
<i className='bi bi-facebook'></i> Login with Facebook
</button>
<button
className='w-100 py-2 mb-2 btn btn-outline-primary rounded-3'
type='button'
>
<i className='bi bi-twitch'></i> Login with Twitch
</button>
</form>
)
return content
}
export default Login
And the modal code
import { useEffect, useCallback } from 'react'
import { Outlet, useNavigate, useLocation } from 'react-router-dom'
const Modal = () => {
const location = useLocation()
const navigate = useNavigate()
// Closes modal when Escape is pressed
const handleEscKey = useCallback(
(event) => {
if (event.key === 'Escape') {
navigate(-1)
}
},
[navigate]
)
// Listens for Key to close modal
useEffect(() => {
document.addEventListener('keyup', handleEscKey, false)
return () => {
document.removeEventListener('keyup', handleEscKey, false)
}
}, [handleEscKey])
const content = (
// Modal background
<div
className='modal d-block bg-body-tertiary bg-opacity-50 py-md-5'
tabIndex='-1'
role='dialog'
id='modal'
onClick={() => navigate(-1)}
>
<div // Modal body
className='modal-dialog modal-fullscreen-sm-down modal-dialog-centered'
role='document'
onClick={(e) => e.stopPropagation()}
>
<div className='modal-content rounded-4 shadow'>
<div className='modal-header p-5 pb-4 border-bottom-0'>
<h1 className='fw-bold mb-0 fs-2 ms-auto'>
{location.state.context}
</h1>
<button
type='button'
className='btn-close'
aria-label='Close'
onClick={() => navigate(-1)}
></button>
</div>
<div className='modal-body p-5 pt-0'>
<Outlet />
</div>
</div>
</div>
</div>
)
return content
}
export default Modal
To make the input field focusable on mobile devices, you can use the onClick event handler rather than onFocus. This will allow users to tap on the field to give focus which will automatically open the keyboard on the smartphones.
Update:
Added e.stopPropagation()
to the onClick
handler of the input field. This will prevent the modal from receiving the click event and allow the input field to receive focus instead.
Here is the example:
import React, { useRef } from 'react';
const Input = ({
label,
type,
name,
placeholder,
autoComplete,
required,
className,
disabled,
onChange,
}) => {
const inputRef = useRef(null);
const handleFocus = () => {
if (inputRef.current) {
inputRef.current.focus();
}
};
return (
<div className={`${!!label ? 'form-floating' : ''}`}>
<input
ref={inputRef}
className={`form-control ${className ?? ''}`}
type={type}
name={name}
id={name}
placeholder={placeholder}
required={required}
autoComplete={autoComplete}
disabled={disabled}
onChange={(e) => onChange({ payload: e })}
onClick={(e) => {
e.stopPropagation(); // Stop event propagation here
handleFocus();
}}
/>
{!!label && <label htmlFor={name}>{placeholder}</label>}
</div>
);
};
export default Input;