Input.tsx 5.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203
  1. import React from 'react';
  2. import { RegisterOptions, useFormContext } from 'react-hook-form';
  3. import SearchIcon from 'components/common/Icons/SearchIcon';
  4. import { ErrorMessage } from '@hookform/error-message';
  5. import * as S from './Input.styled';
  6. import { InputLabel } from './InputLabel.styled';
  7. export interface InputProps
  8. extends React.InputHTMLAttributes<HTMLInputElement>,
  9. Omit<S.InputProps, 'search'> {
  10. name?: string;
  11. hookFormOptions?: RegisterOptions;
  12. search?: boolean;
  13. positiveOnly?: boolean;
  14. withError?: boolean;
  15. label?: React.ReactNode;
  16. hint?: React.ReactNode;
  17. clearIcon?: React.ReactNode;
  18. // Some may only accept integer, like `Number of Partitions`
  19. // some may accept decimal
  20. integerOnly?: boolean;
  21. }
  22. function inputNumberCheck(
  23. key: string,
  24. positiveOnly: boolean,
  25. integerOnly: boolean,
  26. getValues: (name: string) => string,
  27. componentName: string
  28. ) {
  29. let isValid = true;
  30. if (!((key >= '0' && key <= '9') || key === '-' || key === '.')) {
  31. // If not a valid digit char.
  32. isValid = false;
  33. } else {
  34. // If there is any restriction.
  35. if (positiveOnly) {
  36. isValid = !(key === '-');
  37. }
  38. if (isValid && integerOnly) {
  39. isValid = !(key === '.');
  40. }
  41. // Check invalid format
  42. const value = getValues(componentName);
  43. if (isValid && (key === '-' || key === '.')) {
  44. if (!positiveOnly) {
  45. if (key === '-') {
  46. if (value !== '') {
  47. // '-' should not appear anywhere except the start of the string
  48. isValid = false;
  49. }
  50. }
  51. }
  52. if (!integerOnly) {
  53. if (key === '.') {
  54. if (value === '' || value.indexOf('.') !== -1) {
  55. // '.' should not appear at the start of the string or appear twice
  56. isValid = false;
  57. }
  58. }
  59. }
  60. }
  61. }
  62. return isValid;
  63. }
  64. function pasteNumberCheck(
  65. text: string,
  66. positiveOnly: boolean,
  67. integerOnly: boolean
  68. ) {
  69. let value: string;
  70. value = text;
  71. let sign = '';
  72. if (!positiveOnly) {
  73. if (value.charAt(0) === '-') {
  74. sign = '-';
  75. }
  76. }
  77. if (integerOnly) {
  78. value = value.replace(/\D/g, '');
  79. } else {
  80. value = value.replace(/[^\d.]/g, '');
  81. if (value.indexOf('.') !== value.lastIndexOf('.')) {
  82. const strs = value.split('.');
  83. value = '';
  84. for (let i = 0; i < strs.length; i += 1) {
  85. value += strs[i];
  86. if (i === 0) {
  87. value += '.';
  88. }
  89. }
  90. }
  91. }
  92. value = sign + value;
  93. return value;
  94. }
  95. const Input = React.forwardRef<HTMLInputElement, InputProps>((props, ref) => {
  96. const {
  97. name,
  98. hookFormOptions,
  99. search,
  100. inputSize = 'L',
  101. type,
  102. positiveOnly,
  103. integerOnly,
  104. withError = false,
  105. label,
  106. hint,
  107. clearIcon,
  108. ...rest
  109. } = props;
  110. const methods = useFormContext();
  111. const fieldId = React.useId();
  112. const isHookFormField = !!name && !!methods.register;
  113. const keyPressEventHandler = (
  114. event: React.KeyboardEvent<HTMLInputElement>
  115. ) => {
  116. const { key } = event;
  117. if (type === 'number') {
  118. // Manually prevent input of non-digit and non-minus for all number inputs
  119. // and prevent input of negative numbers for positiveOnly inputs
  120. if (
  121. !inputNumberCheck(
  122. key,
  123. typeof positiveOnly === 'boolean' ? positiveOnly : false,
  124. typeof integerOnly === 'boolean' ? integerOnly : false,
  125. methods.getValues,
  126. typeof name === 'string' ? name : ''
  127. )
  128. ) {
  129. event.preventDefault();
  130. }
  131. }
  132. };
  133. const pasteEventHandler = (event: React.ClipboardEvent<HTMLInputElement>) => {
  134. if (type === 'number') {
  135. const { clipboardData } = event;
  136. // The 'clipboardData' does not have key 'Text', but has key 'text' instead.
  137. const text = clipboardData.getData('text');
  138. // Check the format of pasted text.
  139. const value = pasteNumberCheck(
  140. text,
  141. typeof positiveOnly === 'boolean' ? positiveOnly : false,
  142. typeof integerOnly === 'boolean' ? integerOnly : false
  143. );
  144. // if paste value contains non-numeric characters or
  145. // negative for positiveOnly fields then prevent paste
  146. if (value !== text) {
  147. event.preventDefault();
  148. // for react-hook-form fields only set transformed value
  149. if (isHookFormField) {
  150. methods.setValue(name, value);
  151. }
  152. }
  153. }
  154. };
  155. let inputOptions = { ...rest };
  156. if (isHookFormField) {
  157. // extend input options with react-hook-form options
  158. // if the field is a part of react-hook-form form
  159. inputOptions = { ...rest, ...methods.register(name, hookFormOptions) };
  160. }
  161. return (
  162. <div>
  163. {label && <InputLabel htmlFor={rest.id || fieldId}>{label}</InputLabel>}
  164. <S.Wrapper>
  165. {search && <SearchIcon />}
  166. <S.Input
  167. id={fieldId}
  168. inputSize={inputSize}
  169. search={!!search}
  170. type={type}
  171. onKeyPress={keyPressEventHandler}
  172. onPaste={pasteEventHandler}
  173. ref={ref}
  174. {...inputOptions}
  175. />
  176. {clearIcon}
  177. {withError && isHookFormField && (
  178. <S.FormError>
  179. <ErrorMessage name={name} />
  180. </S.FormError>
  181. )}
  182. {hint && <S.InputHint>{hint}</S.InputHint>}
  183. </S.Wrapper>
  184. </div>
  185. );
  186. });
  187. export default Input;