ClosableTextInput.tsx 3.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138
  1. import React, {
  2. FC, memo, useEffect, useRef, useState,
  3. } from 'react';
  4. import { useTranslation } from 'next-i18next';
  5. import { AlertInfo, AlertType, inputValidator } from '~/client/util/input-validator';
  6. type ClosableTextInputProps = {
  7. value?: string
  8. placeholder?: string
  9. validationTarget?: string,
  10. onPressEnter?(inputText: string | null): void
  11. onClickOutside?(): void
  12. handleInputChange?: (string) => void
  13. }
  14. const ClosableTextInput: FC<ClosableTextInputProps> = memo((props: ClosableTextInputProps) => {
  15. const { t } = useTranslation();
  16. const { validationTarget } = props;
  17. const inputRef = useRef<HTMLInputElement>(null);
  18. const [inputText, setInputText] = useState(props.value);
  19. const [currentAlertInfo, setAlertInfo] = useState<AlertInfo | null>(null);
  20. const [isAbleToShowAlert, setIsAbleToShowAlert] = useState(false);
  21. const [isComposing, setComposing] = useState(false);
  22. const createValidation = async(inputText: string) => {
  23. const alertInfo = await inputValidator(inputText, validationTarget);
  24. if (alertInfo && alertInfo.message != null && alertInfo.target != null) {
  25. alertInfo.message = t(alertInfo.message, { target: t(alertInfo.target) });
  26. }
  27. setAlertInfo(alertInfo);
  28. };
  29. const onChangeHandler = async(e: React.ChangeEvent<HTMLInputElement>) => {
  30. const inputText = e.target.value;
  31. createValidation(inputText);
  32. setInputText(inputText);
  33. setIsAbleToShowAlert(true);
  34. if (props.handleInputChange != null) {
  35. props.handleInputChange(inputText);
  36. }
  37. };
  38. const onFocusHandler = async(e: React.ChangeEvent<HTMLInputElement>) => {
  39. const inputText = e.target.value;
  40. await createValidation(inputText);
  41. };
  42. const onPressEnter = () => {
  43. if (props.onPressEnter != null) {
  44. const text = inputText != null ? inputText.trim() : null;
  45. if (currentAlertInfo == null) {
  46. props.onPressEnter(text);
  47. }
  48. }
  49. };
  50. const onKeyDownHandler = (e) => {
  51. switch (e.key) {
  52. case 'Enter':
  53. // Do nothing when composing
  54. if (isComposing) {
  55. return;
  56. }
  57. onPressEnter();
  58. break;
  59. default:
  60. break;
  61. }
  62. };
  63. /*
  64. * Hide when click outside the ref
  65. */
  66. const onBlurHandler = () => {
  67. if (props.onClickOutside == null) {
  68. return;
  69. }
  70. props.onClickOutside();
  71. };
  72. // didMount
  73. useEffect(() => {
  74. // autoFocus
  75. if (inputRef?.current == null) {
  76. return;
  77. }
  78. inputRef.current.focus();
  79. });
  80. const AlertInfo = () => {
  81. if (currentAlertInfo == null) {
  82. return <></>;
  83. }
  84. const alertType = currentAlertInfo.type != null ? currentAlertInfo.type : AlertType.ERROR;
  85. const alertMessage = currentAlertInfo.message != null ? currentAlertInfo.message : 'Invalid value';
  86. const alertTextStyle = alertType === AlertType.ERROR ? 'text-danger' : 'text-warning';
  87. const translation = alertType === AlertType.ERROR ? 'Error' : 'Warning';
  88. return (
  89. <p className={`${alertTextStyle} text-center mt-1`}>{t(translation)}: {alertMessage}</p>
  90. );
  91. };
  92. return (
  93. <div>
  94. <input
  95. value={inputText || ''}
  96. ref={inputRef}
  97. type="text"
  98. className="form-control"
  99. placeholder={props.placeholder}
  100. name="input"
  101. data-testid="closable-text-input"
  102. onFocus={onFocusHandler}
  103. onChange={onChangeHandler}
  104. onKeyDown={onKeyDownHandler}
  105. onCompositionStart={() => setComposing(true)}
  106. onCompositionEnd={() => setComposing(false)}
  107. onBlur={onBlurHandler}
  108. autoFocus={false}
  109. />
  110. {isAbleToShowAlert && <AlertInfo />}
  111. </div>
  112. );
  113. });
  114. ClosableTextInput.displayName = 'ClosableTextInput';
  115. export default ClosableTextInput;