ClosableTextInput.tsx 3.5 KB

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