use-submittable.ts 2.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106
  1. import type React from 'react';
  2. import type { CompositionEvent } from 'react';
  3. import { useCallback, useState } from 'react';
  4. import type { SubmittableInputProps } from './types';
  5. export const useSubmittable = (
  6. props: SubmittableInputProps,
  7. ): Partial<React.InputHTMLAttributes<HTMLInputElement>> => {
  8. const {
  9. value,
  10. onChange,
  11. onBlur,
  12. onCompositionStart,
  13. onCompositionEnd,
  14. onSubmit,
  15. onCancel,
  16. } = props;
  17. const [inputText, setInputText] = useState(value ?? '');
  18. const [lastSubmittedInputText, setLastSubmittedInputText] = useState<
  19. string | undefined
  20. >(value ?? '');
  21. const [isComposing, setComposing] = useState(false);
  22. const changeHandler = useCallback(
  23. async (e: React.ChangeEvent<HTMLInputElement>) => {
  24. const inputText = e.target.value;
  25. setInputText(inputText);
  26. onChange?.(e);
  27. },
  28. [onChange],
  29. );
  30. const keyDownHandler = useCallback(
  31. (e) => {
  32. switch (e.key) {
  33. case 'Enter':
  34. // Do nothing when composing
  35. if (isComposing) {
  36. return;
  37. }
  38. setLastSubmittedInputText(inputText);
  39. onSubmit?.(inputText.trim());
  40. break;
  41. case 'Escape':
  42. if (isComposing) {
  43. return;
  44. }
  45. onCancel?.();
  46. break;
  47. }
  48. },
  49. [inputText, isComposing, onCancel, onSubmit],
  50. );
  51. const blurHandler = useCallback(
  52. (e) => {
  53. // suppress continuous calls to submit by blur event
  54. if (lastSubmittedInputText === inputText) {
  55. return;
  56. }
  57. // submit on blur
  58. setLastSubmittedInputText(inputText);
  59. onSubmit?.(inputText.trim());
  60. onBlur?.(e);
  61. },
  62. [inputText, lastSubmittedInputText, onSubmit, onBlur],
  63. );
  64. const compositionStartHandler = useCallback(
  65. (e: CompositionEvent<HTMLInputElement>) => {
  66. setComposing(true);
  67. onCompositionStart?.(e);
  68. },
  69. [onCompositionStart],
  70. );
  71. const compositionEndHandler = useCallback(
  72. (e: CompositionEvent<HTMLInputElement>) => {
  73. setComposing(false);
  74. onCompositionEnd?.(e);
  75. },
  76. [onCompositionEnd],
  77. );
  78. const {
  79. // eslint-disable-next-line @typescript-eslint/no-unused-vars
  80. value: _value,
  81. onSubmit: _onSubmit,
  82. onCancel: _onCancel,
  83. ...cleanedProps
  84. } = props;
  85. return {
  86. ...cleanedProps,
  87. value: inputText,
  88. onChange: changeHandler,
  89. onKeyDown: keyDownHandler,
  90. onBlur: blurHandler,
  91. onCompositionStart: compositionStartHandler,
  92. onCompositionEnd: compositionEndHandler,
  93. };
  94. };