@transferwise/components 0.0.0-experimental-85b9dd1 → 0.0.0-experimental-0db2ae7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (151) hide show
  1. package/build/index.esm.js +335 -234
  2. package/build/index.esm.js.map +1 -1
  3. package/build/index.js +336 -235
  4. package/build/index.js.map +1 -1
  5. package/build/types/common/locale/index.d.ts +43 -26
  6. package/build/types/common/locale/index.d.ts.map +1 -1
  7. package/build/types/common/textFormat/formatWithPattern/formatWithPattern.d.ts +1 -1
  8. package/build/types/common/textFormat/formatWithPattern/formatWithPattern.d.ts.map +1 -1
  9. package/build/types/common/textFormat/getCursorPositionAfterKeystroke/getCursorPositionAfterKeystroke.d.ts +2 -2
  10. package/build/types/common/textFormat/getCursorPositionAfterKeystroke/getCursorPositionAfterKeystroke.d.ts.map +1 -1
  11. package/build/types/common/textFormat/getSymbolsInPatternWithPosition/getSymbolsInPatternWithPosition.d.ts +5 -1
  12. package/build/types/common/textFormat/getSymbolsInPatternWithPosition/getSymbolsInPatternWithPosition.d.ts.map +1 -1
  13. package/build/types/common/textFormat/unformatWithPattern/unformatWithPattern.d.ts +1 -1
  14. package/build/types/common/textFormat/unformatWithPattern/unformatWithPattern.d.ts.map +1 -1
  15. package/build/types/index.d.ts +2 -0
  16. package/build/types/index.d.ts.map +1 -1
  17. package/build/types/inputWithDisplayFormat/InputWithDisplayFormat.d.ts +7 -11
  18. package/build/types/inputWithDisplayFormat/InputWithDisplayFormat.d.ts.map +1 -1
  19. package/build/types/inputWithDisplayFormat/index.d.ts +2 -1
  20. package/build/types/inputWithDisplayFormat/index.d.ts.map +1 -1
  21. package/build/types/phoneNumberInput/PhoneNumberInput.d.ts +27 -22
  22. package/build/types/phoneNumberInput/PhoneNumberInput.d.ts.map +1 -1
  23. package/build/types/phoneNumberInput/data/countries.d.ts +10 -5
  24. package/build/types/phoneNumberInput/data/countries.d.ts.map +1 -1
  25. package/build/types/phoneNumberInput/index.d.ts +1 -1
  26. package/build/types/phoneNumberInput/index.d.ts.map +1 -1
  27. package/build/types/phoneNumberInput/utils/cleanNumber/cleanNumber.d.ts +1 -1
  28. package/build/types/phoneNumberInput/utils/cleanNumber/cleanNumber.d.ts.map +1 -1
  29. package/build/types/phoneNumberInput/utils/cleanNumber/index.d.ts +1 -1
  30. package/build/types/phoneNumberInput/utils/cleanNumber/index.d.ts.map +1 -1
  31. package/build/types/phoneNumberInput/utils/excludeCountries/excludeCountries.d.ts +1 -8
  32. package/build/types/phoneNumberInput/utils/excludeCountries/excludeCountries.d.ts.map +1 -1
  33. package/build/types/phoneNumberInput/utils/excludeCountries/index.d.ts +1 -1
  34. package/build/types/phoneNumberInput/utils/excludeCountries/index.d.ts.map +1 -1
  35. package/build/types/phoneNumberInput/utils/explodeNumberModel/index.d.ts +4 -8
  36. package/build/types/phoneNumberInput/utils/explodeNumberModel/index.d.ts.map +1 -1
  37. package/build/types/phoneNumberInput/utils/filterOptionsForQuery/index.d.ts +2 -0
  38. package/build/types/phoneNumberInput/utils/filterOptionsForQuery/index.d.ts.map +1 -0
  39. package/build/types/phoneNumberInput/utils/findCountryByCode/index.d.ts +1 -1
  40. package/build/types/phoneNumberInput/utils/findCountryByCode/index.d.ts.map +1 -1
  41. package/build/types/phoneNumberInput/utils/findCountryByPrefix/index.d.ts +1 -1
  42. package/build/types/phoneNumberInput/utils/findCountryByPrefix/index.d.ts.map +1 -1
  43. package/build/types/phoneNumberInput/utils/groupCountriesByPrefix/groupCountriesByPrefix.d.ts +1 -2
  44. package/build/types/phoneNumberInput/utils/groupCountriesByPrefix/groupCountriesByPrefix.d.ts.map +1 -1
  45. package/build/types/phoneNumberInput/utils/groupCountriesByPrefix/index.d.ts +1 -1
  46. package/build/types/phoneNumberInput/utils/groupCountriesByPrefix/index.d.ts.map +1 -1
  47. package/build/types/phoneNumberInput/utils/index.d.ts +13 -11
  48. package/build/types/phoneNumberInput/utils/index.d.ts.map +1 -1
  49. package/build/types/phoneNumberInput/utils/isOptionAndFitsQuery/index.d.ts +2 -0
  50. package/build/types/phoneNumberInput/utils/isOptionAndFitsQuery/index.d.ts.map +1 -0
  51. package/build/types/phoneNumberInput/utils/isOptionAndFitsQuery/isOptionAndFitsQuery.d.ts +3 -0
  52. package/build/types/phoneNumberInput/utils/isOptionAndFitsQuery/isOptionAndFitsQuery.d.ts.map +1 -0
  53. package/build/types/phoneNumberInput/utils/isStringNumeric/index.d.ts +1 -1
  54. package/build/types/phoneNumberInput/utils/isStringNumeric/index.d.ts.map +1 -1
  55. package/build/types/phoneNumberInput/utils/isStringNumeric/isStringNumeric.d.ts +1 -1
  56. package/build/types/phoneNumberInput/utils/isStringNumeric/isStringNumeric.d.ts.map +1 -1
  57. package/build/types/phoneNumberInput/utils/isValidPhoneNumber/index.d.ts +1 -1
  58. package/build/types/phoneNumberInput/utils/isValidPhoneNumber/index.d.ts.map +1 -1
  59. package/build/types/phoneNumberInput/utils/isValidPhoneNumber/isValidPhoneNumber.d.ts +1 -6
  60. package/build/types/phoneNumberInput/utils/isValidPhoneNumber/isValidPhoneNumber.d.ts.map +1 -1
  61. package/build/types/phoneNumberInput/utils/longestMatchingPrefix/index.d.ts +1 -2
  62. package/build/types/phoneNumberInput/utils/longestMatchingPrefix/index.d.ts.map +1 -1
  63. package/build/types/phoneNumberInput/utils/setDefaultPrefix/index.d.ts +1 -7
  64. package/build/types/phoneNumberInput/utils/setDefaultPrefix/index.d.ts.map +1 -1
  65. package/build/types/phoneNumberInput/utils/sortArrayByProperty/index.d.ts +1 -1
  66. package/build/types/phoneNumberInput/utils/sortArrayByProperty/index.d.ts.map +1 -1
  67. package/build/types/phoneNumberInput/utils/sortArrayByProperty/sortArrayByProperty.d.ts +1 -1
  68. package/build/types/phoneNumberInput/utils/sortArrayByProperty/sortArrayByProperty.d.ts.map +1 -1
  69. package/build/types/textareaWithDisplayFormat/TextareaWithDisplayFormat.d.ts +7 -11
  70. package/build/types/textareaWithDisplayFormat/TextareaWithDisplayFormat.d.ts.map +1 -1
  71. package/build/types/textareaWithDisplayFormat/index.d.ts +2 -1
  72. package/build/types/textareaWithDisplayFormat/index.d.ts.map +1 -1
  73. package/build/types/withDisplayFormat/WithDisplayFormat.d.ts +54 -82
  74. package/build/types/withDisplayFormat/WithDisplayFormat.d.ts.map +1 -1
  75. package/build/types/withDisplayFormat/index.d.ts +2 -1
  76. package/build/types/withDisplayFormat/index.d.ts.map +1 -1
  77. package/package.json +1 -1
  78. package/src/common/locale/index.js +139 -0
  79. package/src/common/locale/{index.spec.ts → index.spec.js} +4 -4
  80. package/src/common/textFormat/formatWithPattern/{formatWithPattern.js → formatWithPattern.ts} +8 -4
  81. package/src/common/textFormat/getCursorPositionAfterKeystroke/{getCursorPositionAfterKeystroke.js → getCursorPositionAfterKeystroke.ts} +8 -8
  82. package/src/common/textFormat/getSymbolsInPatternWithPosition/{getSymbolsInPatternWithPosition.js → getSymbolsInPatternWithPosition.ts} +7 -2
  83. package/src/common/textFormat/unformatWithPattern/{unformatWithPattern.js → unformatWithPattern.ts} +3 -2
  84. package/src/index.ts +2 -0
  85. package/src/inputWithDisplayFormat/InputWithDisplayFormat.tsx +10 -0
  86. package/src/inputWithDisplayFormat/index.ts +2 -0
  87. package/src/phoneNumberInput/PhoneNumberInput.js +210 -0
  88. package/src/phoneNumberInput/PhoneNumberInput.spec.js +22 -18
  89. package/src/phoneNumberInput/data/{countries.ts → countries.js} +1 -9
  90. package/src/phoneNumberInput/data/countries.spec.js +12 -0
  91. package/src/phoneNumberInput/utils/cleanNumber/cleanNumber.js +4 -0
  92. package/src/phoneNumberInput/utils/excludeCountries/{excludeCountries.ts → excludeCountries.js} +5 -6
  93. package/src/phoneNumberInput/utils/excludeCountries/{excludeCountries.spec.ts → excludeCountries.spec.js} +1 -1
  94. package/src/phoneNumberInput/utils/explodeNumberModel/{explodeNumberModel.spec.ts → explodeNumberModel.spec.js} +1 -1
  95. package/src/phoneNumberInput/utils/explodeNumberModel/index.js +27 -0
  96. package/src/phoneNumberInput/utils/filterOptionsForQuery/filterOptionsForQuery.spec.js +36 -0
  97. package/src/phoneNumberInput/utils/filterOptionsForQuery/index.js +11 -0
  98. package/src/phoneNumberInput/utils/findCountryByCode/{findCountryByCode.spec.ts → findCountryByCode.spec.js} +1 -0
  99. package/src/phoneNumberInput/utils/findCountryByCode/index.js +10 -0
  100. package/src/phoneNumberInput/utils/findCountryByPrefix/index.js +11 -0
  101. package/src/phoneNumberInput/utils/groupCountriesByPrefix/groupCountriesByPrefix.js +26 -0
  102. package/src/phoneNumberInput/utils/groupCountriesByPrefix/groupCountriesByPrefix.spec.js +67 -0
  103. package/src/phoneNumberInput/utils/{index.ts → index.js} +2 -0
  104. package/src/phoneNumberInput/utils/isOptionAndFitsQuery/index.js +1 -0
  105. package/src/phoneNumberInput/utils/isOptionAndFitsQuery/isOptionAndFitsQuery.js +25 -0
  106. package/src/phoneNumberInput/utils/isOptionAndFitsQuery/isOptionAndFitsQuery.spec.js +66 -0
  107. package/src/phoneNumberInput/utils/isStringNumeric/isStringNumeric.js +1 -0
  108. package/src/phoneNumberInput/utils/isStringNumeric/{isStringNumeric.spec.ts → isStringNumeric.spec.js} +1 -0
  109. package/src/phoneNumberInput/utils/isValidPhoneNumber/isValidPhoneNumber.js +10 -0
  110. package/src/phoneNumberInput/utils/isValidPhoneNumber/{isValidPhoneNumber.spec.ts → isValidPhoneNumber.spec.js} +1 -1
  111. package/src/phoneNumberInput/utils/longestMatchingPrefix/index.js +2 -0
  112. package/src/phoneNumberInput/utils/setDefaultPrefix/index.js +25 -0
  113. package/src/phoneNumberInput/utils/sortArrayByProperty/sortArrayByProperty.js +3 -0
  114. package/src/textareaWithDisplayFormat/TextareaWithDisplayFormat.spec.js +3 -1
  115. package/src/textareaWithDisplayFormat/TextareaWithDisplayFormat.story.tsx +32 -0
  116. package/src/textareaWithDisplayFormat/TextareaWithDisplayFormat.tsx +13 -0
  117. package/src/textareaWithDisplayFormat/index.ts +2 -0
  118. package/src/withDisplayFormat/WithDisplayFormat.spec.js +1 -1
  119. package/src/withDisplayFormat/{WithDisplayFormat.js → WithDisplayFormat.tsx} +127 -107
  120. package/src/withDisplayFormat/index.ts +2 -0
  121. package/src/common/locale/index.ts +0 -96
  122. package/src/inputWithDisplayFormat/InputWithDisplayFormat.js +0 -14
  123. package/src/inputWithDisplayFormat/index.js +0 -1
  124. package/src/phoneNumberInput/PhoneNumberInput.tsx +0 -193
  125. package/src/phoneNumberInput/utils/cleanNumber/cleanNumber.ts +0 -3
  126. package/src/phoneNumberInput/utils/explodeNumberModel/index.ts +0 -24
  127. package/src/phoneNumberInput/utils/findCountryByCode/index.ts +0 -12
  128. package/src/phoneNumberInput/utils/findCountryByPrefix/index.ts +0 -12
  129. package/src/phoneNumberInput/utils/groupCountriesByPrefix/groupCountriesByPrefix.spec.ts +0 -102
  130. package/src/phoneNumberInput/utils/groupCountriesByPrefix/groupCountriesByPrefix.ts +0 -12
  131. package/src/phoneNumberInput/utils/isStringNumeric/isStringNumeric.ts +0 -1
  132. package/src/phoneNumberInput/utils/isValidPhoneNumber/isValidPhoneNumber.ts +0 -7
  133. package/src/phoneNumberInput/utils/longestMatchingPrefix/index.ts +0 -4
  134. package/src/phoneNumberInput/utils/setDefaultPrefix/index.ts +0 -20
  135. package/src/phoneNumberInput/utils/sortArrayByProperty/sortArrayByProperty.ts +0 -6
  136. package/src/textareaWithDisplayFormat/TextareaWithDisplayFormat.js +0 -14
  137. package/src/textareaWithDisplayFormat/index.js +0 -1
  138. package/src/withDisplayFormat/index.js +0 -1
  139. /package/src/phoneNumberInput/{PhoneNumberInput.story.tsx → PhoneNumberInput.story.js} +0 -0
  140. /package/src/phoneNumberInput/{index.ts → index.js} +0 -0
  141. /package/src/phoneNumberInput/utils/cleanNumber/{cleanNumber.spec.ts → cleanNumber.spec.js} +0 -0
  142. /package/src/phoneNumberInput/utils/cleanNumber/{index.ts → index.js} +0 -0
  143. /package/src/phoneNumberInput/utils/excludeCountries/{index.ts → index.js} +0 -0
  144. /package/src/phoneNumberInput/utils/findCountryByPrefix/{findCountryByPrefix.spec.ts → findCountryByPrefix.spec.js} +0 -0
  145. /package/src/phoneNumberInput/utils/groupCountriesByPrefix/{index.ts → index.js} +0 -0
  146. /package/src/phoneNumberInput/utils/isStringNumeric/{index.ts → index.js} +0 -0
  147. /package/src/phoneNumberInput/utils/isValidPhoneNumber/{index.ts → index.js} +0 -0
  148. /package/src/phoneNumberInput/utils/longestMatchingPrefix/{longestMatchingPrefix.spec.ts → longestMatchingPrefix.spec.js} +0 -0
  149. /package/src/phoneNumberInput/utils/setDefaultPrefix/{setDefaultPrefix.spec.ts → setDefaultPrefix.spec.js} +0 -0
  150. /package/src/phoneNumberInput/utils/sortArrayByProperty/{index.ts → index.js} +0 -0
  151. /package/src/phoneNumberInput/utils/sortArrayByProperty/{sortArrayByProperty.spec.ts → sortArrayByProperty.spec.js} +0 -0
@@ -0,0 +1,25 @@
1
+ import { isArray } from '@transferwise/neptune-validation';
2
+ /**
3
+ * Checks if query is contained into object properties.
4
+ *
5
+ * @param {object} option - the select option
6
+ * @param {string} query - the current search query
7
+ * @returns {boolean}
8
+ */
9
+ export const isOptionAndFitsQuery = (option, query) =>
10
+ startsWith(option.iso3, query) ||
11
+ startsWith(option.iso2, query) ||
12
+ startsWith(option.name, query) ||
13
+ startsWith(option.phone, query);
14
+
15
+ export const startsWith = (property, query) => {
16
+ if (isArray(property)) {
17
+ return (
18
+ property.filter((proper) => normalizeValue(proper).indexOf(normalizeValue(query)) === 0)
19
+ .length > 0
20
+ );
21
+ }
22
+ return normalizeValue(property).indexOf(normalizeValue(query)) === 0;
23
+ };
24
+
25
+ const normalizeValue = (value) => value.toLowerCase().replace('+', '');
@@ -0,0 +1,66 @@
1
+ import { startsWith, isOptionAndFitsQuery } from '.';
2
+
3
+ const DATA_TEST = [
4
+ {
5
+ name: 'test1',
6
+ iso2: 'TT',
7
+ iso3: 'TT1',
8
+ phone: '+93',
9
+ },
10
+ {
11
+ name: 'test1',
12
+ iso2: ['TT', 'AA'],
13
+ iso3: 'TT1',
14
+ phone: '+93',
15
+ },
16
+ {
17
+ name: 'something',
18
+ iso2: 'ST',
19
+ iso3: 'SMT',
20
+ phone: '+33',
21
+ },
22
+ ];
23
+
24
+ describe('isOptionAndFitsQuery', () => {
25
+ describe('when option is given', () => {
26
+ it('should return true if query is relevant', () => {
27
+ expect(isOptionAndFitsQuery(DATA_TEST[0], 'TT')).toBe(true);
28
+ });
29
+
30
+ it('should return true if query is relevant and one of the array values', () => {
31
+ expect(isOptionAndFitsQuery(DATA_TEST[1], 'TT')).toBe(true);
32
+ });
33
+
34
+ it('should return false if query is not relevant and not one of the array values', () => {
35
+ expect(isOptionAndFitsQuery(DATA_TEST[1], 'BB')).toBe(false);
36
+ });
37
+
38
+ it('should return false if query is not relevant', () => {
39
+ expect(isOptionAndFitsQuery(DATA_TEST[0], 'AA')).toBe(false);
40
+ });
41
+ });
42
+ });
43
+
44
+ describe('startWith', () => {
45
+ describe('when property is given', () => {
46
+ it('returns true if any of the values starts with the query', () => {
47
+ expect(startsWith('AA', 'AA')).toBe(true);
48
+ });
49
+
50
+ it(`returns false if value doesn't start with`, () => {
51
+ expect(startsWith('AABB', 'BB')).toBe(false);
52
+ });
53
+
54
+ it('should return true if query is contained in grouped options', () => {
55
+ expect(startsWith(['AA', 'BB'], 'BB')).toBe(true);
56
+ });
57
+
58
+ it("returns false if any value in an array doesn't start with", () => {
59
+ expect(startsWith(['CCAA', 'CCBB'], 'BB')).toBe(false);
60
+ });
61
+
62
+ it('returns false for an empty value', () => {
63
+ expect(startsWith('', 'BB')).toBe(false);
64
+ });
65
+ });
66
+ });
@@ -0,0 +1 @@
1
+ export const isStringNumeric = (value) => /^\+?[\d-\s]+$/.test(value);
@@ -4,6 +4,7 @@ describe('isStringNumeric', () => {
4
4
  it('should return true when numeric sting is provided', () => {
5
5
  expect(isStringNumeric('+23456')).toBe(true);
6
6
  expect(isStringNumeric('23456')).toBe(true);
7
+ expect(isStringNumeric(23456)).toBe(true);
7
8
  });
8
9
 
9
10
  it('should return false when string is provided', () => {
@@ -0,0 +1,10 @@
1
+ /**
2
+ *
3
+ * @param phoneNumber
4
+ * @returns {boolean} - returns true for number that starts with '+' and contains a mix of digits and spaces with
5
+ * at least 4 digits.
6
+ */
7
+ export const isValidPhoneNumber = (phoneNumber) =>
8
+ /^\+[\d-\s]+$/.test(phoneNumber) &&
9
+ phoneNumber.match(/\d+/g) &&
10
+ phoneNumber.match(/\d+/g).join('').length >= 4;
@@ -11,7 +11,7 @@ describe('isValidPhoneNumber', () => {
11
11
 
12
12
  it('should return false for invalid numbers', () => {
13
13
  expect(isValidPhoneNumber('+441')).toBe(false);
14
- expect(isValidPhoneNumber('44')).toBe(false);
14
+ expect(isValidPhoneNumber(44)).toBe(false);
15
15
  expect(isValidPhoneNumber('44123')).toBe(false);
16
16
  });
17
17
  });
@@ -0,0 +1,2 @@
1
+ export const longestMatchingPrefix = (matchingCodes) =>
2
+ matchingCodes.reduce((a, b) => (a.phone.length > b.phone.length ? a : b));
@@ -0,0 +1,25 @@
1
+ import { getCountryFromLocale } from '../../../common/locale';
2
+ import { findCountryByCode } from '../findCountryByCode';
3
+
4
+ /**
5
+ * Default phone code, the UK one `+44`
6
+ */
7
+ const DEFAULT_PHONE_CODE = '+44';
8
+
9
+ /**
10
+ * Given a valid locale it returns the correspondent prefix if found or +44 otherwise.
11
+ *
12
+ * @param {string} locale - a string that represent the locale ex:'es-ES'
13
+ * @param countryCode
14
+ * @returns {string}
15
+ */
16
+ export const setDefaultPrefix = (locale, countryCode) => {
17
+ const country =
18
+ findCountryByCode(countryCode) ||
19
+ // when `locale` code has explicit region: `en-GB`, `en-US`, `ar-AE`
20
+ findCountryByCode(getCountryFromLocale(locale)) ||
21
+ // when `locale` code is only two chars value: `fr`, `es`
22
+ findCountryByCode(locale);
23
+
24
+ return country?.phone || DEFAULT_PHONE_CODE;
25
+ };
@@ -0,0 +1,3 @@
1
+ // Reference fro localeCompare : https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/localeCompare
2
+ export const sortArrayByProperty = (arrayToSort, property) =>
3
+ [...arrayToSort].sort((a, b) => a[property].localeCompare(b[property]));
@@ -1,5 +1,7 @@
1
1
  import { shallow } from 'enzyme';
2
2
 
3
+ import { TextArea } from '../inputs/TextArea';
4
+
3
5
  import TextareaWithDisplayFormat from '.';
4
6
 
5
7
  describe('TextareaWithDisplayFormat', () => {
@@ -10,6 +12,6 @@ describe('TextareaWithDisplayFormat', () => {
10
12
  .find('WithDisplayFormat')
11
13
  .renderProp('render')({ value: 'test' });
12
14
 
13
- expect(view.find('textarea').props('value')).toStrictEqual({ value: 'test' });
15
+ expect(view.find(TextArea).props('value')).toStrictEqual({ value: 'test' });
14
16
  });
15
17
  });
@@ -0,0 +1,32 @@
1
+ import { StoryObj } from '@storybook/react';
2
+
3
+ import { userEvent, within } from '../test-utils';
4
+
5
+ import TextareaWithDisplayFormat from './TextareaWithDisplayFormat';
6
+
7
+ export default {
8
+ component: TextareaWithDisplayFormat,
9
+ title: 'Forms/TextareaWithDisplayFormat',
10
+ };
11
+
12
+ type Story = StoryObj<typeof TextareaWithDisplayFormat>;
13
+
14
+ export const Basic: Story = {
15
+ render: (args) => {
16
+ return (
17
+ <>
18
+ <TextareaWithDisplayFormat
19
+ value="0000"
20
+ displayPattern="**** - **** - ****"
21
+ onChange={console.log}
22
+ />
23
+ </>
24
+ );
25
+ },
26
+ // intentionally use interactive typing (over init value via `value` prop)
27
+ // to trigger event handlers in the component
28
+ play: ({ canvasElement }) => {
29
+ const canvas = within(canvasElement);
30
+ userEvent.type(canvas.getByRole('textbox'), '111122223333');
31
+ },
32
+ };
@@ -0,0 +1,13 @@
1
+ import { TextArea, type TextAreaProps } from '../inputs/TextArea';
2
+ import WithDisplayFormat, { type WithDisplayFormatProps } from '../withDisplayFormat';
3
+
4
+ export interface TextareaWithDisplayFormatProps extends Omit<WithDisplayFormatProps, 'render'> {}
5
+
6
+ const TextareaWithDisplayFormat = (props: TextareaWithDisplayFormatProps) => (
7
+ <WithDisplayFormat<TextAreaProps>
8
+ {...props}
9
+ render={(renderProps) => <TextArea {...renderProps} />}
10
+ />
11
+ );
12
+
13
+ export default TextareaWithDisplayFormat;
@@ -0,0 +1,2 @@
1
+ export { default } from './TextareaWithDisplayFormat';
2
+ export type { TextareaWithDisplayFormatProps } from './TextareaWithDisplayFormat';
@@ -145,7 +145,7 @@ describe('InputWithTextFormat', () => {
145
145
  });
146
146
 
147
147
  describe('set cursor position', () => {
148
- const triggerEventA = { ...triggerEvent, target: { setSelectionRange: () => {} } };
148
+ const triggerEventA = { ...triggerEvent, currentTarget: { setSelectionRange: () => {} } };
149
149
  beforeEach(() => {
150
150
  component.setState({
151
151
  triggerEvent: triggerEventA,
@@ -1,5 +1,4 @@
1
- import PropTypes from 'prop-types';
2
- import { Component } from 'react';
1
+ import { Component, KeyboardEvent, ClipboardEvent, ChangeEvent, FocusEvent } from 'react';
3
2
 
4
3
  import { HistoryNavigator } from '../common';
5
4
  import {
@@ -10,27 +9,93 @@ import {
10
9
  getDistanceToPreviousSymbol,
11
10
  getDistanceToNextSymbol,
12
11
  } from '../common/textFormat';
12
+ import { InputProps } from '../inputs/Input';
13
+ import { TextAreaProps } from '../inputs/TextArea';
14
+
15
+ type HTMLTextElement = HTMLInputElement | HTMLTextAreaElement;
16
+ type TypeElementAttrs = InputProps | TextAreaProps;
17
+
18
+ export type EventType =
19
+ | 'KeyDown'
20
+ | 'Paste'
21
+ | 'Cut'
22
+ | 'Undo'
23
+ | 'Redo'
24
+ | 'Backspace'
25
+ | 'Delete'
26
+ | 'Initial';
27
+
28
+ interface WithDisplayFormatState {
29
+ value: string;
30
+ historyNavigator: HistoryNavigator;
31
+ prevDisplayPattern: string;
32
+ triggerType: EventType;
33
+ triggerEvent: KeyboardEvent<HTMLTextElement> | null;
34
+ pastedLength: number;
35
+ selectionStart: number;
36
+ selectionEnd: number;
37
+ }
38
+
39
+ export interface WithDisplayFormatProps<T extends TypeElementAttrs = TypeElementAttrs>
40
+ extends Pick<
41
+ TypeElementAttrs,
42
+ | 'className'
43
+ | 'disabled'
44
+ | 'id'
45
+ | 'maxLength'
46
+ | 'minLength'
47
+ | 'name'
48
+ | 'placeholder'
49
+ | 'readOnly'
50
+ | 'required'
51
+ | 'inputMode'
52
+ > {
53
+ value?: string;
54
+ displayPattern: string;
55
+ /**
56
+ * autocomplete hides our form help so we need to disable it when help text
57
+ * is present. Chrome ignores autocomplete=off, the only way to disable it is
58
+ * to provide an 'invalid' value, for which 'disabled' serves.
59
+ */
60
+ autoComplete?: TypeElementAttrs['autoComplete'] | 'disabled';
61
+ onChange?: (value: string) => void;
62
+ onBlur?: (value: string) => void;
63
+ onFocus?: (value: string) => void;
64
+ render: (renderProps: T) => JSX.Element;
65
+ }
13
66
 
14
- class WithDisplayFormat extends Component {
15
- constructor(props) {
67
+ class WithDisplayFormat<T extends TypeElementAttrs> extends Component<
68
+ WithDisplayFormatProps<T>,
69
+ WithDisplayFormatState
70
+ > {
71
+ declare props: WithDisplayFormatProps<T> &
72
+ Required<Pick<WithDisplayFormatProps<T>, keyof typeof WithDisplayFormat.defaultProps>>;
73
+ static defaultProps = {
74
+ autoComplete: 'off',
75
+ displayPattern: '',
76
+ value: '',
77
+ };
78
+
79
+ constructor(props: WithDisplayFormatProps) {
16
80
  super(props);
17
- const { value, displayPattern } = props;
18
- const unformattedValue = unformatWithPattern(value, displayPattern);
81
+ const unformattedValue = unformatWithPattern(props.value ?? '', props.displayPattern);
19
82
  this.state = {
20
- value: formatWithPattern(unformattedValue, displayPattern),
83
+ value: formatWithPattern(unformattedValue, props.displayPattern),
21
84
  historyNavigator: new HistoryNavigator(),
22
85
  prevDisplayPattern: props.displayPattern,
23
- triggerType: null,
86
+ triggerType: 'Initial',
24
87
  triggerEvent: null,
88
+ selectionStart: 0,
89
+ selectionEnd: 0,
90
+ pastedLength: 0,
25
91
  };
26
92
  }
27
93
 
28
- static getDerivedStateFromProps(nextProps, previousState) {
29
- const { displayPattern } = nextProps;
30
- const { prevDisplayPattern } = previousState;
31
- if (previousState.prevDisplayPattern !== displayPattern) {
32
- const { value, historyNavigator } = previousState;
33
-
94
+ static getDerivedStateFromProps(
95
+ { displayPattern }: WithDisplayFormatProps,
96
+ { prevDisplayPattern = displayPattern, value, historyNavigator }: WithDisplayFormatState,
97
+ ) {
98
+ if (prevDisplayPattern !== displayPattern) {
34
99
  const unFormattedValue = unformatWithPattern(value, prevDisplayPattern);
35
100
  historyNavigator.reset();
36
101
 
@@ -45,43 +110,48 @@ class WithDisplayFormat extends Component {
45
110
  return null;
46
111
  }
47
112
 
48
- getUserAction = (unformattedValue) => {
113
+ getUserAction = (unformattedValue: string): EventType | string => {
49
114
  const { triggerEvent, triggerType, value } = this.state;
50
115
  const { displayPattern } = this.props;
51
116
 
52
- const charCode = String.fromCharCode(triggerEvent.which).toLowerCase();
117
+ if (triggerEvent) {
118
+ const charCode = String.fromCharCode(triggerEvent.which).toLowerCase();
53
119
 
54
- if (triggerType === 'Paste' || triggerType === 'Cut') {
55
- return triggerType;
56
- }
120
+ if (triggerType === 'Paste' || triggerType === 'Cut') {
121
+ return triggerType;
122
+ }
57
123
 
58
- if ((triggerEvent.ctrlKey || triggerEvent.metaKey) && charCode === 'z') {
59
- return triggerEvent.shiftKey ? 'Redo' : 'Undo';
60
- }
61
- // Detect mouse event redo
62
- if (triggerEvent.ctrlKey && charCode === 'd') {
63
- return 'Delete';
64
- }
124
+ if ((triggerEvent.ctrlKey || triggerEvent.metaKey) && charCode === 'z') {
125
+ return triggerEvent.shiftKey ? 'Redo' : 'Undo';
126
+ }
127
+ // Detect mouse event redo
128
+ if (triggerEvent.ctrlKey && charCode === 'd') {
129
+ return 'Delete';
130
+ }
65
131
 
66
- // Android Fix.
67
- if (typeof triggerEvent.key === 'undefined') {
68
- if (unformattedValue.length <= unformatWithPattern(value, displayPattern).length) {
132
+ // Android Fix.
133
+ if (
134
+ typeof triggerEvent.key === 'undefined' &&
135
+ unformattedValue.length <= unformatWithPattern(value, displayPattern).length
136
+ ) {
69
137
  return 'Backspace';
70
138
  }
139
+ return triggerEvent.key;
140
+ } else {
141
+ // triggerEvent can be null only in case of "autofilling" (via password manager extension or browser build-in one) events
142
+ return 'Paste';
71
143
  }
72
-
73
- return triggerEvent.key;
74
144
  };
75
145
 
76
146
  resetEvent = () => {
77
147
  this.setState({
78
- triggerType: null,
148
+ triggerType: 'Initial',
79
149
  triggerEvent: null,
80
150
  pastedLength: 0,
81
151
  });
82
152
  };
83
153
 
84
- detectUndoRedo = (event) => {
154
+ detectUndoRedo = (event: KeyboardEvent<HTMLTextElement>) => {
85
155
  const charCode = String.fromCharCode(event.which).toLowerCase();
86
156
  if ((event.ctrlKey || event.metaKey) && charCode === 'z') {
87
157
  return event.shiftKey ? 'Redo' : 'Undo';
@@ -89,9 +159,9 @@ class WithDisplayFormat extends Component {
89
159
  return null;
90
160
  };
91
161
 
92
- handleOnKeyDown = (event) => {
162
+ handleOnKeyDown = (event: KeyboardEvent<HTMLTextElement>) => {
93
163
  event.persist();
94
- const { selectionStart, selectionEnd } = event.target;
164
+ const { selectionStart, selectionEnd } = event.currentTarget;
95
165
  const { historyNavigator } = this.state;
96
166
  const { displayPattern } = this.props;
97
167
 
@@ -108,13 +178,13 @@ class WithDisplayFormat extends Component {
108
178
  this.setState({
109
179
  triggerEvent: event,
110
180
  triggerType: 'KeyDown',
111
- selectionStart,
112
- selectionEnd,
181
+ selectionStart: selectionStart ?? 0,
182
+ selectionEnd: selectionEnd ?? 0,
113
183
  });
114
184
  }
115
185
  };
116
186
 
117
- handleOnPaste = (event) => {
187
+ handleOnPaste = (event: ClipboardEvent<HTMLTextElement>) => {
118
188
  const { displayPattern } = this.props;
119
189
  const pastedLength = unformatWithPattern(
120
190
  event.clipboardData.getData('Text'),
@@ -128,23 +198,19 @@ class WithDisplayFormat extends Component {
128
198
  this.setState({ triggerType: 'Cut' });
129
199
  };
130
200
 
131
- isKeyAllowed = (action) => {
201
+ isKeyAllowed = (action: EventType | string) => {
132
202
  const { displayPattern } = this.props;
133
203
  const symbolsInPattern = displayPattern.split('').filter((character) => character !== '*');
134
204
 
135
205
  return !symbolsInPattern.includes(action);
136
206
  };
137
207
 
138
- handleOnChange = (event) => {
139
- const { historyNavigator, triggerEvent, triggerType } = this.state;
208
+ handleOnChange = (event: ChangeEvent<HTMLTextElement>) => {
209
+ const { historyNavigator, triggerType } = this.state;
140
210
  const { displayPattern, onChange } = this.props;
141
211
  const { value } = event.target;
142
212
  let unformattedValue = unformatWithPattern(value, displayPattern);
143
- const action =
144
- triggerEvent === null
145
- ? // triggerEvent can be null only in case of "autofilling" (via password manager extension or browser build-in one) events
146
- 'Paste'
147
- : this.getUserAction(unformattedValue);
213
+ const action = this.getUserAction(unformattedValue);
148
214
  if (!this.isKeyAllowed(action) || triggerType === 'Undo' || triggerType === 'Redo') {
149
215
  return;
150
216
  }
@@ -158,19 +224,20 @@ class WithDisplayFormat extends Component {
158
224
 
159
225
  this.handleCursorPositioning(action);
160
226
 
161
- const broadcastValue = unformatWithPattern(newFormattedValue, displayPattern);
162
-
163
- this.setState({ value: newFormattedValue }, this.resetEvent(), onChange(broadcastValue));
227
+ this.setState({ value: newFormattedValue }, () => {
228
+ this.resetEvent();
229
+ if (onChange) {
230
+ const broadcastValue = unformatWithPattern(newFormattedValue, displayPattern);
231
+ onChange(broadcastValue);
232
+ }
233
+ });
164
234
  };
165
235
 
166
- handleOnBlur = (event) => {
167
- const { displayPattern, onBlur } = this.props;
168
- if (onBlur) {
169
- onBlur(unformatWithPattern(event.target.value, displayPattern));
170
- }
236
+ handleOnBlur = (event: FocusEvent<HTMLTextElement>) => {
237
+ this.props.onBlur?.(unformatWithPattern(event.target.value, this.props.displayPattern));
171
238
  };
172
239
 
173
- handleOnFocus = (event) => {
240
+ handleOnFocus = (event: FocusEvent<HTMLTextElement>) => {
174
241
  const { displayPattern, onFocus } = this.props;
175
242
  if (onFocus) {
176
243
  this.handleOnChange(event);
@@ -178,7 +245,7 @@ class WithDisplayFormat extends Component {
178
245
  }
179
246
  };
180
247
 
181
- handleDelete = (unformattedValue, action) => {
248
+ handleDelete = (unformattedValue: string, action: EventType) => {
182
249
  const { displayPattern } = this.props;
183
250
  const { selectionStart, selectionEnd } = this.state;
184
251
  const newStack = [...unformattedValue];
@@ -203,7 +270,7 @@ class WithDisplayFormat extends Component {
203
270
  return newStack.join('');
204
271
  };
205
272
 
206
- handleCursorPositioning = (action) => {
273
+ handleCursorPositioning = (action: string) => {
207
274
  const { displayPattern } = this.props;
208
275
  const { triggerEvent, selectionStart, selectionEnd, pastedLength } = this.state;
209
276
 
@@ -217,7 +284,7 @@ class WithDisplayFormat extends Component {
217
284
 
218
285
  setTimeout(() => {
219
286
  if (triggerEvent) {
220
- triggerEvent.target.setSelectionRange(cursorPosition, cursorPosition);
287
+ triggerEvent.currentTarget.setSelectionRange(cursorPosition, cursorPosition);
221
288
  }
222
289
  this.setState({ selectionStart: cursorPosition, selectionEnd: cursorPosition });
223
290
  }, 0);
@@ -225,7 +292,6 @@ class WithDisplayFormat extends Component {
225
292
 
226
293
  render() {
227
294
  const {
228
- type,
229
295
  inputMode,
230
296
  className,
231
297
  id,
@@ -239,8 +305,7 @@ class WithDisplayFormat extends Component {
239
305
  autoComplete,
240
306
  } = this.props;
241
307
  const { value } = this.state;
242
- const renderProps = {
243
- type,
308
+ const renderProps: TypeElementAttrs = {
244
309
  inputMode,
245
310
  className,
246
311
  id,
@@ -260,53 +325,8 @@ class WithDisplayFormat extends Component {
260
325
  onChange: this.handleOnChange,
261
326
  onCut: this.handleOnCut,
262
327
  };
263
- return this.props.render(renderProps);
328
+ return this.props.render(renderProps as T);
264
329
  }
265
330
  }
266
331
 
267
- WithDisplayFormat.propTypes = {
268
- /**
269
- * autocomplete hides our form help so we need to disable it when help text
270
- * is present. Chrome ignores autocomplete=off, the only way to disable it is
271
- * to provide an 'invalid' value, for which 'disabled' serves.
272
- */
273
- autoComplete: PropTypes.oneOf(['on', 'off', 'disabled']),
274
- className: PropTypes.string,
275
- disabled: PropTypes.bool,
276
- id: PropTypes.string,
277
- maxLength: PropTypes.number,
278
- minLength: PropTypes.number,
279
- name: PropTypes.string,
280
- onFocus: PropTypes.func,
281
- onBlur: PropTypes.func,
282
- onChange: PropTypes.func.isRequired,
283
- placeholder: PropTypes.string,
284
- readOnly: PropTypes.bool,
285
- render: PropTypes.func.isRequired,
286
- required: PropTypes.bool,
287
- displayPattern: PropTypes.string,
288
- type: PropTypes.string,
289
- inputMode: PropTypes.string,
290
- value: PropTypes.string,
291
- };
292
-
293
- WithDisplayFormat.defaultProps = {
294
- autoComplete: 'off',
295
- className: null,
296
- disabled: false,
297
- id: null,
298
- maxLength: null,
299
- minLength: null,
300
- name: null,
301
- placeholder: null,
302
- readOnly: false,
303
- required: false,
304
- displayPattern: '',
305
- type: 'text',
306
- inputMode: null,
307
- value: '',
308
- onFocus: null,
309
- onBlur: null,
310
- };
311
-
312
332
  export default WithDisplayFormat;
@@ -0,0 +1,2 @@
1
+ export { default } from './WithDisplayFormat';
2
+ export type { WithDisplayFormatProps } from './WithDisplayFormat';