ClosableTextInput.tsx 2.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120
  1. import React, {
  2. FC, memo, useEffect, useRef, useState,
  3. } from 'react';
  4. import { useTranslation } from 'react-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. isShown: boolean
  16. value?: string
  17. placeholder?: string
  18. inputValidator?(text: string): AlertInfo | Promise<AlertInfo> | null
  19. onPressEnter?(inputText: string | null): void
  20. onClickOutside?(): void
  21. }
  22. const ClosableTextInput: FC<ClosableTextInputProps> = memo((props: ClosableTextInputProps) => {
  23. const { t } = useTranslation();
  24. const inputRef = useRef<HTMLInputElement>(null);
  25. const [inputText, setInputText] = useState(props.value);
  26. const [currentAlertInfo, setAlertInfo] = useState<AlertInfo | null>(null);
  27. const onChangeHandler = async(e) => {
  28. if (props.inputValidator == null) { return }
  29. const inputText = e.target.value;
  30. const alertInfo = await props.inputValidator(inputText);
  31. setAlertInfo(alertInfo);
  32. setInputText(inputText);
  33. };
  34. const onPressEnter = () => {
  35. if (props.onPressEnter == null) {
  36. return;
  37. }
  38. const text = inputText != null ? inputText.trim() : null;
  39. props.onPressEnter(text);
  40. };
  41. const onKeyDownHandler = (e) => {
  42. switch (e.key) {
  43. case 'Enter':
  44. onPressEnter();
  45. break;
  46. default:
  47. break;
  48. }
  49. };
  50. /*
  51. * Hide when click outside the ref
  52. */
  53. const onBlurHandler = () => {
  54. if (props.onClickOutside == null) {
  55. return;
  56. }
  57. props.onClickOutside();
  58. };
  59. // didMount
  60. useEffect(() => {
  61. // autoFocus
  62. if (inputRef?.current == null) {
  63. return;
  64. }
  65. inputRef.current.focus();
  66. });
  67. const AlertInfo = () => {
  68. if (currentAlertInfo == null) {
  69. return <></>;
  70. }
  71. const alertType = currentAlertInfo.type != null ? currentAlertInfo.type : AlertType.ERROR;
  72. const alertMessage = currentAlertInfo.message != null ? currentAlertInfo.message : 'Invalid value';
  73. const alertTextStyle = alertType === AlertType.ERROR ? 'text-danger' : 'text-warning';
  74. const translation = alertType === AlertType.ERROR ? 'Error' : 'Warning';
  75. return (
  76. <p className={`${alertTextStyle} text-center mt-1`}>{t(translation)}: {alertMessage}</p>
  77. );
  78. };
  79. return (
  80. <div className={props.isShown ? 'd-block' : 'd-none'}>
  81. <input
  82. value={inputText}
  83. ref={inputRef}
  84. type="text"
  85. className="form-control"
  86. placeholder={props.placeholder}
  87. name="input"
  88. onChange={onChangeHandler}
  89. onKeyDown={onKeyDownHandler}
  90. onBlur={onBlurHandler}
  91. autoFocus={false}
  92. />
  93. <AlertInfo />
  94. </div>
  95. );
  96. });
  97. export default ClosableTextInput;