Вопрос или проблема
Этот вопрос, похоже, задавался несколько раз, но я пока не нашел окончательного решения. Я разрабатываю компонент калькулятора, удобного для мобильных устройств, с использованием React и shadcn.
Вот базовая структура
<div className="w-full max-w-md mx-auto p-4 rounded-lg">
<Input
type="text"
value={input}
onChange={handleInputChange}
onKeyDown={handleKeyDown}
className="w-full mb-4 text-left text-lg font-mono text-black dark:text-white cursor-text"
aria-label="Ввод калькулятора"
id='calculator-input'
inputMode="none"
/>
{error && (
<Alert variant="destructive" className="mb-4">
<AlertCircle className="h-4 w-4"/>
<AlertDescription>{error}</AlertDescription>
</Alert>
)}
<div className="grid grid-cols-4 gap-2">
{buttons.map((btn) => (
<Button
key={btn}
onClick={() => handleButtonClick(btn)}
className="h-12"
useGlobalStyles={false}
variant={buttonVariants[btn] || 'default'}
aria-label={btn}
>
{btn === 'Backspace' ? <Delete className="h-4 w-4"/> :
btn === 'Enter' ? <ArrowRight className="h-4 w-4 text-black dark:text-white"/> :
btn}
</Button>
))}
</div>
</div>
Логика кнопок, которая добавляет/удаляет значения в позиции курсора
const handleButtonClick = (value: string) => {
setError(null)
if (value === 'Enter') {
handleSubmit()
} else if (value === 'Backspace') {
setInput(prev => {
const inputElement = document.getElementById('calculator-input') as HTMLInputElement;
const cursorPosition = inputElement.selectionStart || 0;
const newValue = (prev || '').toString().slice(0, cursorPosition - 1) + (prev || '').toString().slice(cursorPosition);
setTimeout(() => {
inputElement.setSelectionRange(cursorPosition - 1, cursorPosition - 1);
}, 0);
return newValue;
})
setIsEvaluated(false)
} else if (value === '=') {
if (isEvaluated) {
handleSubmit()
} else {
handleEvaluate()
}
} else {
setInput(prev => {
const inputElement = document.getElementById('calculator-input') as HTMLInputElement;
const cursorPosition = inputElement.selectionStart || 0;
const newValue = (prev || '').toString().slice(0, cursorPosition) + value + (prev || '').toString().slice(cursorPosition);
setTimeout(() => {
inputElement.setSelectionRange(cursorPosition + value.length, cursorPosition + value.length);
}, 0);
return newValue;
});
setIsEvaluated(false)
}
}
Использование inputMode="none"
не кажется предотвращающим появление клавиатуры на мобильных устройствах, любые решения по этому поводу будут оценены. Также буду признателен за советы по более элегантной логике взаимодействия с курсором.
Спасибо
Ответ или решение
Чтобы предотвратить появление виртуальной клавиатуры при взаимодействии с полем ввода на мобильных устройствах, но позволить пользователю управлять курсором, есть несколько подходов, которые вы можете рассмотреть. Ваша текущая реализация с использованием inputMode="none"
не всегда гарантирует отключение клавиатуры, поэтому важно применить другие методы.
1. Использование contentEditable
Вместо поля ввода <input>
, вы можете использовать элемент div
с атрибутом contentEditable
, что позволит пользователю использовать курсор и редактировать текст, при этом клавиатура не будет автоматически открываться. Вот пример того, как это можно реализовать:
<div
contentEditable
role="textbox"
className="w-full mb-4 text-left text-lg font-mono text-black dark:text-white cursor-text"
aria-label="Calculator input"
onInput={handleInputChange}
onKeyDown={handleKeyDown}
onFocus={(e) => e.target.setAttribute('tabindex', '0')}
onBlur={(e) => e.target.removeAttribute('tabindex')}
id="calculator-input"
></div>
2. Настройка событий для управления курсором
Ваша логика для управления курсором может быть реорганизована, чтобы обеспечить более гибкое изменеие позиции курсора. Вот пример для обработки ввода с помощью contentEditable
:
const handleInputChange = (e) => {
setInput(e.target.innerText);
};
const handleButtonClick = (value) => {
setError(null);
const inputElement = document.getElementById('calculator-input');
const selectionStart = window.getSelection().focusOffset;
if (value === 'Enter') {
handleSubmit();
} else if (value === 'Backspace') {
setInput((prev) => {
const newValue = prev.slice(0, selectionStart - 1) + prev.slice(selectionStart);
setTimeout(() => {
setCursorPosition(inputElement, selectionStart - 1);
}, 0);
return newValue;
});
} else {
setInput((prev) => {
const newValue = prev.slice(0, selectionStart) + value + prev.slice(selectionStart);
setTimeout(() => {
setCursorPosition(inputElement, selectionStart + value.length);
}, 0);
return newValue;
});
}
};
const setCursorPosition = (element, position) => {
const range = document.createRange();
const sel = window.getSelection();
range.setStart(element.childNodes[0], position);
range.collapse(true);
sel.removeAllRanges();
sel.addRange(range);
};
3. Стиль и доступность
Не забудьте добавить необходимые атрибуты для обеспечения доступности, такие как role="textbox"
и aria-label
, которые улучшат взаимодействие для пользователей с ограниченными возможностями.
Заключение
Использование contentEditable
предоставляет вам необходимую гибкость для управления текстом без открытия клавиатуры. Это также освобождает вас от необходимости работать с inputMode
. Обеспечьте контроль за курсором и редактированием текста с помощью обработчиков событий, чтобы создать удобный и функциональный интерфейс калькулятора.