247 lines
8.9 KiB
JavaScript
247 lines
8.9 KiB
JavaScript
/*
|
||
Предрейсовые осмотры - мобильное приложение
|
||
Компонент модального окна с полем ввода
|
||
*/
|
||
|
||
//---------------------
|
||
//Подключение библиотек
|
||
//---------------------
|
||
|
||
const React = require('react'); //React
|
||
const { Modal, View, TextInput, Pressable, Platform, StyleSheet, BackHandler } = require('react-native'); //Базовые компоненты
|
||
const AppText = require('./AppText'); //Общий текстовый компонент
|
||
const AppButton = require('./AppButton'); //Кнопка
|
||
const styles = require('../../styles/common/InputDialog.styles'); //Стили диалога
|
||
|
||
const OVERLAY_Z = 9999;
|
||
|
||
//-----------
|
||
//Тело модуля
|
||
//-----------
|
||
|
||
//Модальное окно с полем ввода
|
||
function InputDialog(
|
||
{
|
||
visible,
|
||
title = 'Ввод данных',
|
||
label,
|
||
value = '',
|
||
placeholder,
|
||
keyboardType = 'default',
|
||
autoCapitalize = 'none',
|
||
confirmText = 'Сохранить',
|
||
cancelText = 'Отмена',
|
||
onConfirm,
|
||
onCancel,
|
||
validator,
|
||
errorMessage
|
||
},
|
||
ref
|
||
) {
|
||
//Локальное значение для редактирования
|
||
const [inputValue, setInputValue] = React.useState(value);
|
||
const inputRef = React.useRef(null);
|
||
//На Android поле ввода изначально не фокусируемо, чтобы нативный слой видел «нет фокуса» и перехватывал сканер с первого символа (как на остальных экранах)
|
||
const [inputFocusable, setInputFocusable] = React.useState(Platform.OS !== 'android');
|
||
|
||
const focusFirstEditableInput = React.useCallback(() => {
|
||
if (inputRef.current && typeof inputRef.current.focus === 'function') {
|
||
inputRef.current.focus();
|
||
}
|
||
}, []);
|
||
|
||
React.useImperativeHandle(
|
||
ref,
|
||
function exposeScannerApi() {
|
||
return {
|
||
setValueFromScanner(val) {
|
||
setInputValue(val != null ? String(val) : '');
|
||
if (Platform.OS === 'android') setInputFocusable(true);
|
||
focusFirstEditableInput();
|
||
}
|
||
};
|
||
},
|
||
[focusFirstEditableInput]
|
||
);
|
||
|
||
//После включения фокусируемости на Android — ставим фокус на поле (после setValueFromScanner)
|
||
React.useEffect(
|
||
function focusWhenFocusable() {
|
||
if (Platform.OS !== 'android' || !inputFocusable) return;
|
||
focusFirstEditableInput();
|
||
},
|
||
[inputFocusable, focusFirstEditableInput]
|
||
);
|
||
const [error, setError] = React.useState('');
|
||
const [isFocused, setIsFocused] = React.useState(false);
|
||
|
||
//Сброс значения и фокусируемости только при открытии диалога
|
||
const prevVisibleRef = React.useRef(false);
|
||
React.useEffect(() => {
|
||
const justOpened = visible && !prevVisibleRef.current;
|
||
prevVisibleRef.current = visible;
|
||
if (justOpened) {
|
||
setInputValue(value);
|
||
setError('');
|
||
if (Platform.OS === 'android') setInputFocusable(false);
|
||
}
|
||
}, [visible, value]);
|
||
|
||
//Кнопка «Назад» на Android при overlay (без Modal) закрывает диалог
|
||
React.useEffect(
|
||
function backHandler() {
|
||
if (Platform.OS !== 'android' || !visible) return;
|
||
const sub = BackHandler.addEventListener('hardwareBackPress', function onBack() {
|
||
handleCancel();
|
||
return true;
|
||
});
|
||
return function remove() {
|
||
sub.remove();
|
||
};
|
||
},
|
||
[visible, handleCancel]
|
||
);
|
||
|
||
//Обработчик фокуса
|
||
const handleFocus = React.useCallback(() => {
|
||
setIsFocused(true);
|
||
}, []);
|
||
|
||
//Обработчик потери фокуса
|
||
const handleBlur = React.useCallback(() => {
|
||
setIsFocused(false);
|
||
}, []);
|
||
|
||
//Обработчик изменения текста
|
||
const handleChangeText = React.useCallback(text => {
|
||
setInputValue(text);
|
||
setError('');
|
||
}, []);
|
||
|
||
//Валидация введённого значения
|
||
const validateInput = React.useCallback(() => {
|
||
if (typeof validator === 'function') {
|
||
const validationResult = validator(inputValue);
|
||
if (validationResult !== true) {
|
||
setError(validationResult || errorMessage || 'Некорректное значение');
|
||
return false;
|
||
}
|
||
}
|
||
return true;
|
||
}, [inputValue, validator, errorMessage]);
|
||
|
||
//Обработчик подтверждения
|
||
const handleConfirm = React.useCallback(() => {
|
||
if (!validateInput()) {
|
||
return;
|
||
}
|
||
|
||
if (typeof onConfirm === 'function') {
|
||
onConfirm(inputValue.trim());
|
||
}
|
||
}, [inputValue, validateInput, onConfirm]);
|
||
|
||
//Обработчик отмены
|
||
const handleCancel = React.useCallback(() => {
|
||
if (typeof onCancel === 'function') {
|
||
onCancel();
|
||
}
|
||
}, [onCancel]);
|
||
|
||
//Обработчик закрытия по кнопке "Назад" (Android)
|
||
const handleRequestClose = React.useCallback(() => {
|
||
handleCancel();
|
||
}, [handleCancel]);
|
||
|
||
const preventKeyActions = Platform.OS === 'android';
|
||
const closePressableProps = preventKeyActions ? { focusable: false } : {};
|
||
const cancelButtonFocusable = preventKeyActions ? false : undefined;
|
||
const confirmButtonFocusable = preventKeyActions ? false : undefined;
|
||
|
||
const dialogContent = (
|
||
<View style={styles.container}>
|
||
<View style={styles.header}>
|
||
<AppText style={styles.title}>{title}</AppText>
|
||
<Pressable
|
||
accessibilityRole="button"
|
||
accessibilityLabel="Закрыть"
|
||
onPress={handleCancel}
|
||
style={styles.closeButton}
|
||
{...closePressableProps}
|
||
>
|
||
<AppText style={styles.closeButtonText}>×</AppText>
|
||
</Pressable>
|
||
</View>
|
||
|
||
<View style={styles.content}>
|
||
{label ? (
|
||
<AppText style={styles.label} variant="caption" weight="medium">
|
||
{label}
|
||
</AppText>
|
||
) : null}
|
||
|
||
<TextInput
|
||
ref={inputRef}
|
||
style={[styles.input, isFocused && styles.inputFocused, error && styles.inputError]}
|
||
value={inputValue}
|
||
onChangeText={handleChangeText}
|
||
placeholder={placeholder}
|
||
placeholderTextColor={styles.placeholder.color}
|
||
keyboardType={keyboardType}
|
||
autoCapitalize={autoCapitalize}
|
||
multiline={true}
|
||
onFocus={handleFocus}
|
||
onBlur={handleBlur}
|
||
autoFocus={Platform.OS !== 'android'}
|
||
focusable={Platform.OS !== 'android' || inputFocusable}
|
||
selectTextOnFocus={true}
|
||
accessible={true}
|
||
accessibilityLabel={label || placeholder}
|
||
accessibilityRole="text"
|
||
/>
|
||
|
||
{error ? (
|
||
<AppText style={styles.errorText} variant="caption">
|
||
{error}
|
||
</AppText>
|
||
) : null}
|
||
</View>
|
||
|
||
<View style={styles.buttonsRow}>
|
||
<AppButton
|
||
title={cancelText}
|
||
onPress={handleCancel}
|
||
style={styles.cancelButton}
|
||
textStyle={styles.cancelButtonText}
|
||
focusable={cancelButtonFocusable}
|
||
/>
|
||
<AppButton title={confirmText} onPress={handleConfirm} style={styles.confirmButton} focusable={confirmButtonFocusable} />
|
||
</View>
|
||
</View>
|
||
);
|
||
|
||
if (!visible) return null;
|
||
|
||
if (Platform.OS === 'android') {
|
||
return (
|
||
<View style={[StyleSheet.absoluteFill, { zIndex: OVERLAY_Z, elevation: OVERLAY_Z }]} pointerEvents="auto">
|
||
<View style={styles.backdrop} pointerEvents="auto">
|
||
{dialogContent}
|
||
</View>
|
||
</View>
|
||
);
|
||
}
|
||
|
||
return (
|
||
<Modal transparent={true} animationType="fade" statusBarTranslucent={true} visible={true} onRequestClose={handleRequestClose}>
|
||
<View style={styles.backdrop}>{dialogContent}</View>
|
||
</Modal>
|
||
);
|
||
}
|
||
|
||
//----------------
|
||
//Интерфейс модуля
|
||
//----------------
|
||
|
||
module.exports = React.forwardRef(InputDialog);
|