247 lines
8.9 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/*
Предрейсовые осмотры - мобильное приложение
Компонент модального окна с полем ввода
*/
//---------------------
//Подключение библиотек
//---------------------
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);