@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.
- package/build/index.esm.js +335 -234
- package/build/index.esm.js.map +1 -1
- package/build/index.js +336 -235
- package/build/index.js.map +1 -1
- package/build/types/common/locale/index.d.ts +43 -26
- package/build/types/common/locale/index.d.ts.map +1 -1
- package/build/types/common/textFormat/formatWithPattern/formatWithPattern.d.ts +1 -1
- package/build/types/common/textFormat/formatWithPattern/formatWithPattern.d.ts.map +1 -1
- package/build/types/common/textFormat/getCursorPositionAfterKeystroke/getCursorPositionAfterKeystroke.d.ts +2 -2
- package/build/types/common/textFormat/getCursorPositionAfterKeystroke/getCursorPositionAfterKeystroke.d.ts.map +1 -1
- package/build/types/common/textFormat/getSymbolsInPatternWithPosition/getSymbolsInPatternWithPosition.d.ts +5 -1
- package/build/types/common/textFormat/getSymbolsInPatternWithPosition/getSymbolsInPatternWithPosition.d.ts.map +1 -1
- package/build/types/common/textFormat/unformatWithPattern/unformatWithPattern.d.ts +1 -1
- package/build/types/common/textFormat/unformatWithPattern/unformatWithPattern.d.ts.map +1 -1
- package/build/types/index.d.ts +2 -0
- package/build/types/index.d.ts.map +1 -1
- package/build/types/inputWithDisplayFormat/InputWithDisplayFormat.d.ts +7 -11
- package/build/types/inputWithDisplayFormat/InputWithDisplayFormat.d.ts.map +1 -1
- package/build/types/inputWithDisplayFormat/index.d.ts +2 -1
- package/build/types/inputWithDisplayFormat/index.d.ts.map +1 -1
- package/build/types/phoneNumberInput/PhoneNumberInput.d.ts +27 -22
- package/build/types/phoneNumberInput/PhoneNumberInput.d.ts.map +1 -1
- package/build/types/phoneNumberInput/data/countries.d.ts +10 -5
- package/build/types/phoneNumberInput/data/countries.d.ts.map +1 -1
- package/build/types/phoneNumberInput/index.d.ts +1 -1
- package/build/types/phoneNumberInput/index.d.ts.map +1 -1
- package/build/types/phoneNumberInput/utils/cleanNumber/cleanNumber.d.ts +1 -1
- package/build/types/phoneNumberInput/utils/cleanNumber/cleanNumber.d.ts.map +1 -1
- package/build/types/phoneNumberInput/utils/cleanNumber/index.d.ts +1 -1
- package/build/types/phoneNumberInput/utils/cleanNumber/index.d.ts.map +1 -1
- package/build/types/phoneNumberInput/utils/excludeCountries/excludeCountries.d.ts +1 -8
- package/build/types/phoneNumberInput/utils/excludeCountries/excludeCountries.d.ts.map +1 -1
- package/build/types/phoneNumberInput/utils/excludeCountries/index.d.ts +1 -1
- package/build/types/phoneNumberInput/utils/excludeCountries/index.d.ts.map +1 -1
- package/build/types/phoneNumberInput/utils/explodeNumberModel/index.d.ts +4 -8
- package/build/types/phoneNumberInput/utils/explodeNumberModel/index.d.ts.map +1 -1
- package/build/types/phoneNumberInput/utils/filterOptionsForQuery/index.d.ts +2 -0
- package/build/types/phoneNumberInput/utils/filterOptionsForQuery/index.d.ts.map +1 -0
- package/build/types/phoneNumberInput/utils/findCountryByCode/index.d.ts +1 -1
- package/build/types/phoneNumberInput/utils/findCountryByCode/index.d.ts.map +1 -1
- package/build/types/phoneNumberInput/utils/findCountryByPrefix/index.d.ts +1 -1
- package/build/types/phoneNumberInput/utils/findCountryByPrefix/index.d.ts.map +1 -1
- package/build/types/phoneNumberInput/utils/groupCountriesByPrefix/groupCountriesByPrefix.d.ts +1 -2
- package/build/types/phoneNumberInput/utils/groupCountriesByPrefix/groupCountriesByPrefix.d.ts.map +1 -1
- package/build/types/phoneNumberInput/utils/groupCountriesByPrefix/index.d.ts +1 -1
- package/build/types/phoneNumberInput/utils/groupCountriesByPrefix/index.d.ts.map +1 -1
- package/build/types/phoneNumberInput/utils/index.d.ts +13 -11
- package/build/types/phoneNumberInput/utils/index.d.ts.map +1 -1
- package/build/types/phoneNumberInput/utils/isOptionAndFitsQuery/index.d.ts +2 -0
- package/build/types/phoneNumberInput/utils/isOptionAndFitsQuery/index.d.ts.map +1 -0
- package/build/types/phoneNumberInput/utils/isOptionAndFitsQuery/isOptionAndFitsQuery.d.ts +3 -0
- package/build/types/phoneNumberInput/utils/isOptionAndFitsQuery/isOptionAndFitsQuery.d.ts.map +1 -0
- package/build/types/phoneNumberInput/utils/isStringNumeric/index.d.ts +1 -1
- package/build/types/phoneNumberInput/utils/isStringNumeric/index.d.ts.map +1 -1
- package/build/types/phoneNumberInput/utils/isStringNumeric/isStringNumeric.d.ts +1 -1
- package/build/types/phoneNumberInput/utils/isStringNumeric/isStringNumeric.d.ts.map +1 -1
- package/build/types/phoneNumberInput/utils/isValidPhoneNumber/index.d.ts +1 -1
- package/build/types/phoneNumberInput/utils/isValidPhoneNumber/index.d.ts.map +1 -1
- package/build/types/phoneNumberInput/utils/isValidPhoneNumber/isValidPhoneNumber.d.ts +1 -6
- package/build/types/phoneNumberInput/utils/isValidPhoneNumber/isValidPhoneNumber.d.ts.map +1 -1
- package/build/types/phoneNumberInput/utils/longestMatchingPrefix/index.d.ts +1 -2
- package/build/types/phoneNumberInput/utils/longestMatchingPrefix/index.d.ts.map +1 -1
- package/build/types/phoneNumberInput/utils/setDefaultPrefix/index.d.ts +1 -7
- package/build/types/phoneNumberInput/utils/setDefaultPrefix/index.d.ts.map +1 -1
- package/build/types/phoneNumberInput/utils/sortArrayByProperty/index.d.ts +1 -1
- package/build/types/phoneNumberInput/utils/sortArrayByProperty/index.d.ts.map +1 -1
- package/build/types/phoneNumberInput/utils/sortArrayByProperty/sortArrayByProperty.d.ts +1 -1
- package/build/types/phoneNumberInput/utils/sortArrayByProperty/sortArrayByProperty.d.ts.map +1 -1
- package/build/types/textareaWithDisplayFormat/TextareaWithDisplayFormat.d.ts +7 -11
- package/build/types/textareaWithDisplayFormat/TextareaWithDisplayFormat.d.ts.map +1 -1
- package/build/types/textareaWithDisplayFormat/index.d.ts +2 -1
- package/build/types/textareaWithDisplayFormat/index.d.ts.map +1 -1
- package/build/types/withDisplayFormat/WithDisplayFormat.d.ts +54 -82
- package/build/types/withDisplayFormat/WithDisplayFormat.d.ts.map +1 -1
- package/build/types/withDisplayFormat/index.d.ts +2 -1
- package/build/types/withDisplayFormat/index.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/common/locale/index.js +139 -0
- package/src/common/locale/{index.spec.ts → index.spec.js} +4 -4
- package/src/common/textFormat/formatWithPattern/{formatWithPattern.js → formatWithPattern.ts} +8 -4
- package/src/common/textFormat/getCursorPositionAfterKeystroke/{getCursorPositionAfterKeystroke.js → getCursorPositionAfterKeystroke.ts} +8 -8
- package/src/common/textFormat/getSymbolsInPatternWithPosition/{getSymbolsInPatternWithPosition.js → getSymbolsInPatternWithPosition.ts} +7 -2
- package/src/common/textFormat/unformatWithPattern/{unformatWithPattern.js → unformatWithPattern.ts} +3 -2
- package/src/index.ts +2 -0
- package/src/inputWithDisplayFormat/InputWithDisplayFormat.tsx +10 -0
- package/src/inputWithDisplayFormat/index.ts +2 -0
- package/src/phoneNumberInput/PhoneNumberInput.js +210 -0
- package/src/phoneNumberInput/PhoneNumberInput.spec.js +22 -18
- package/src/phoneNumberInput/data/{countries.ts → countries.js} +1 -9
- package/src/phoneNumberInput/data/countries.spec.js +12 -0
- package/src/phoneNumberInput/utils/cleanNumber/cleanNumber.js +4 -0
- package/src/phoneNumberInput/utils/excludeCountries/{excludeCountries.ts → excludeCountries.js} +5 -6
- package/src/phoneNumberInput/utils/excludeCountries/{excludeCountries.spec.ts → excludeCountries.spec.js} +1 -1
- package/src/phoneNumberInput/utils/explodeNumberModel/{explodeNumberModel.spec.ts → explodeNumberModel.spec.js} +1 -1
- package/src/phoneNumberInput/utils/explodeNumberModel/index.js +27 -0
- package/src/phoneNumberInput/utils/filterOptionsForQuery/filterOptionsForQuery.spec.js +36 -0
- package/src/phoneNumberInput/utils/filterOptionsForQuery/index.js +11 -0
- package/src/phoneNumberInput/utils/findCountryByCode/{findCountryByCode.spec.ts → findCountryByCode.spec.js} +1 -0
- package/src/phoneNumberInput/utils/findCountryByCode/index.js +10 -0
- package/src/phoneNumberInput/utils/findCountryByPrefix/index.js +11 -0
- package/src/phoneNumberInput/utils/groupCountriesByPrefix/groupCountriesByPrefix.js +26 -0
- package/src/phoneNumberInput/utils/groupCountriesByPrefix/groupCountriesByPrefix.spec.js +67 -0
- package/src/phoneNumberInput/utils/{index.ts → index.js} +2 -0
- package/src/phoneNumberInput/utils/isOptionAndFitsQuery/index.js +1 -0
- package/src/phoneNumberInput/utils/isOptionAndFitsQuery/isOptionAndFitsQuery.js +25 -0
- package/src/phoneNumberInput/utils/isOptionAndFitsQuery/isOptionAndFitsQuery.spec.js +66 -0
- package/src/phoneNumberInput/utils/isStringNumeric/isStringNumeric.js +1 -0
- package/src/phoneNumberInput/utils/isStringNumeric/{isStringNumeric.spec.ts → isStringNumeric.spec.js} +1 -0
- package/src/phoneNumberInput/utils/isValidPhoneNumber/isValidPhoneNumber.js +10 -0
- package/src/phoneNumberInput/utils/isValidPhoneNumber/{isValidPhoneNumber.spec.ts → isValidPhoneNumber.spec.js} +1 -1
- package/src/phoneNumberInput/utils/longestMatchingPrefix/index.js +2 -0
- package/src/phoneNumberInput/utils/setDefaultPrefix/index.js +25 -0
- package/src/phoneNumberInput/utils/sortArrayByProperty/sortArrayByProperty.js +3 -0
- package/src/textareaWithDisplayFormat/TextareaWithDisplayFormat.spec.js +3 -1
- package/src/textareaWithDisplayFormat/TextareaWithDisplayFormat.story.tsx +32 -0
- package/src/textareaWithDisplayFormat/TextareaWithDisplayFormat.tsx +13 -0
- package/src/textareaWithDisplayFormat/index.ts +2 -0
- package/src/withDisplayFormat/WithDisplayFormat.spec.js +1 -1
- package/src/withDisplayFormat/{WithDisplayFormat.js → WithDisplayFormat.tsx} +127 -107
- package/src/withDisplayFormat/index.ts +2 -0
- package/src/common/locale/index.ts +0 -96
- package/src/inputWithDisplayFormat/InputWithDisplayFormat.js +0 -14
- package/src/inputWithDisplayFormat/index.js +0 -1
- package/src/phoneNumberInput/PhoneNumberInput.tsx +0 -193
- package/src/phoneNumberInput/utils/cleanNumber/cleanNumber.ts +0 -3
- package/src/phoneNumberInput/utils/explodeNumberModel/index.ts +0 -24
- package/src/phoneNumberInput/utils/findCountryByCode/index.ts +0 -12
- package/src/phoneNumberInput/utils/findCountryByPrefix/index.ts +0 -12
- package/src/phoneNumberInput/utils/groupCountriesByPrefix/groupCountriesByPrefix.spec.ts +0 -102
- package/src/phoneNumberInput/utils/groupCountriesByPrefix/groupCountriesByPrefix.ts +0 -12
- package/src/phoneNumberInput/utils/isStringNumeric/isStringNumeric.ts +0 -1
- package/src/phoneNumberInput/utils/isValidPhoneNumber/isValidPhoneNumber.ts +0 -7
- package/src/phoneNumberInput/utils/longestMatchingPrefix/index.ts +0 -4
- package/src/phoneNumberInput/utils/setDefaultPrefix/index.ts +0 -20
- package/src/phoneNumberInput/utils/sortArrayByProperty/sortArrayByProperty.ts +0 -6
- package/src/textareaWithDisplayFormat/TextareaWithDisplayFormat.js +0 -14
- package/src/textareaWithDisplayFormat/index.js +0 -1
- package/src/withDisplayFormat/index.js +0 -1
- /package/src/phoneNumberInput/{PhoneNumberInput.story.tsx → PhoneNumberInput.story.js} +0 -0
- /package/src/phoneNumberInput/{index.ts → index.js} +0 -0
- /package/src/phoneNumberInput/utils/cleanNumber/{cleanNumber.spec.ts → cleanNumber.spec.js} +0 -0
- /package/src/phoneNumberInput/utils/cleanNumber/{index.ts → index.js} +0 -0
- /package/src/phoneNumberInput/utils/excludeCountries/{index.ts → index.js} +0 -0
- /package/src/phoneNumberInput/utils/findCountryByPrefix/{findCountryByPrefix.spec.ts → findCountryByPrefix.spec.js} +0 -0
- /package/src/phoneNumberInput/utils/groupCountriesByPrefix/{index.ts → index.js} +0 -0
- /package/src/phoneNumberInput/utils/isStringNumeric/{index.ts → index.js} +0 -0
- /package/src/phoneNumberInput/utils/isValidPhoneNumber/{index.ts → index.js} +0 -0
- /package/src/phoneNumberInput/utils/longestMatchingPrefix/{longestMatchingPrefix.spec.ts → longestMatchingPrefix.spec.js} +0 -0
- /package/src/phoneNumberInput/utils/setDefaultPrefix/{setDefaultPrefix.spec.ts → setDefaultPrefix.spec.js} +0 -0
- /package/src/phoneNumberInput/utils/sortArrayByProperty/{index.ts → index.js} +0 -0
- /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(
|
|
14
|
+
expect(isValidPhoneNumber(44)).toBe(false);
|
|
15
15
|
expect(isValidPhoneNumber('44123')).toBe(false);
|
|
16
16
|
});
|
|
17
17
|
});
|
|
@@ -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
|
+
};
|
|
@@ -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(
|
|
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;
|
|
@@ -145,7 +145,7 @@ describe('InputWithTextFormat', () => {
|
|
|
145
145
|
});
|
|
146
146
|
|
|
147
147
|
describe('set cursor position', () => {
|
|
148
|
-
const triggerEventA = { ...triggerEvent,
|
|
148
|
+
const triggerEventA = { ...triggerEvent, currentTarget: { setSelectionRange: () => {} } };
|
|
149
149
|
beforeEach(() => {
|
|
150
150
|
component.setState({
|
|
151
151
|
triggerEvent: triggerEventA,
|
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import
|
|
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
|
-
|
|
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
|
|
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:
|
|
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(
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
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
|
-
|
|
117
|
+
if (triggerEvent) {
|
|
118
|
+
const charCode = String.fromCharCode(triggerEvent.which).toLowerCase();
|
|
53
119
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
120
|
+
if (triggerType === 'Paste' || triggerType === 'Cut') {
|
|
121
|
+
return triggerType;
|
|
122
|
+
}
|
|
57
123
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
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
|
-
|
|
67
|
-
|
|
68
|
-
|
|
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:
|
|
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.
|
|
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,
|
|
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
|
-
|
|
162
|
-
|
|
163
|
-
|
|
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
|
-
|
|
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.
|
|
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;
|