ClosableTextInput.tsx 4.1 KB

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