ClosableTextInput.tsx 3.8 KB

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