@servicetitan/form 38.6.0 → 38.6.1

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.
@@ -0,0 +1,2 @@
1
+ import '@testing-library/jest-dom';
2
+ //# sourceMappingURL=phone-number-input-a2.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"phone-number-input-a2.test.d.ts","sourceRoot":"","sources":["../../../src/phone-number-input/__tests__/phone-number-input-a2.test.tsx"],"names":[],"mappings":"AACA,OAAO,2BAA2B,CAAC"}
@@ -36,14 +36,14 @@ export const PhoneNumberInputA2 = (props)=>{
36
36
  if (sip) {
37
37
  return /*#__PURE__*/ _jsx(TextField, {
38
38
  ...restProps,
39
- value: displayValue,
40
- onChange: handleChange,
39
+ value: value,
40
+ onChange: onChange,
41
41
  onFocus: onFocus,
42
42
  onBlur: onBlur,
43
43
  disabled: disabled,
44
44
  readOnly: readOnly,
45
- type: "tel",
46
- placeholder: placeholder
45
+ type: "text",
46
+ placeholder: propPlaceholder
47
47
  });
48
48
  }
49
49
  const renderTextField = (inputProps)=>{
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/phone-number-input/phone-number-input-a2.tsx"],"sourcesContent":["import { ChangeEvent, FC, useCallback } from 'react';\nimport ReactInputMask from 'react-input-mask';\n\nimport { TextField, TextFieldProps } from '@servicetitan/anvil2';\nimport { Culture, CULTURE_TOKEN } from '@servicetitan/culture';\nimport { useOptionalDependencies } from '@servicetitan/react-ioc';\n\ntype PhoneNumberInputA2Props = TextFieldProps & {\n sip?: boolean;\n culture?: Culture;\n};\n\nexport const PhoneNumberInputA2: FC<PhoneNumberInputA2Props> = props => {\n const {\n value,\n onChange,\n placeholder: propPlaceholder,\n culture: propCulture,\n sip,\n onFocus,\n onBlur,\n disabled,\n readOnly,\n ...restProps\n } = props;\n\n const [injectedCulture] = useOptionalDependencies(CULTURE_TOKEN);\n const culture = propCulture ?? injectedCulture;\n const { SimplePhoneMask = '?9999999999', SimplePhonePlaceholder = '' } =\n culture?.PhoneFormat ?? {};\n\n const mask = SimplePhoneMask.replace(/\\?/g, '');\n const placeholder = propPlaceholder ?? SimplePhonePlaceholder;\n const displayValue = String(value ?? '').replace(/\\D/g, '');\n\n // Transform output value to digits-only (unmasked)\n const handleChange = useCallback(\n (event: ChangeEvent<HTMLInputElement>) => {\n if (onChange) {\n const digitsOnly = event.target.value.replace(/\\D/g, '');\n\n const unmaskedEvent = {\n ...event,\n target: { ...event.target, value: digitsOnly },\n currentTarget: { ...event.currentTarget, value: digitsOnly },\n } as ChangeEvent<HTMLInputElement>;\n\n onChange(unmaskedEvent);\n }\n },\n [onChange]\n );\n\n if (sip) {\n return (\n <TextField\n {...restProps}\n value={displayValue}\n onChange={handleChange}\n onFocus={onFocus}\n onBlur={onBlur}\n disabled={disabled}\n readOnly={readOnly}\n type=\"tel\"\n placeholder={placeholder}\n />\n );\n }\n\n const renderTextField = (inputProps: Record<string, unknown>) => {\n // Exclude 'size' as it conflicts with TextField's size prop type\n const { size: UNUSED_SIZE, ...filteredProps } = inputProps;\n return <TextField {...filteredProps} {...restProps} type=\"tel\" placeholder={placeholder} />;\n };\n\n return (\n <ReactInputMask\n value={displayValue}\n onChange={handleChange}\n mask={mask}\n maskChar={null}\n onFocus={onFocus}\n onBlur={onBlur}\n disabled={disabled}\n readOnly={readOnly}\n >\n {renderTextField as any}\n </ReactInputMask>\n );\n};\n"],"names":["useCallback","ReactInputMask","TextField","CULTURE_TOKEN","useOptionalDependencies","PhoneNumberInputA2","props","value","onChange","placeholder","propPlaceholder","culture","propCulture","sip","onFocus","onBlur","disabled","readOnly","restProps","injectedCulture","SimplePhoneMask","SimplePhonePlaceholder","PhoneFormat","mask","replace","displayValue","String","handleChange","event","digitsOnly","target","unmaskedEvent","currentTarget","type","renderTextField","inputProps","size","UNUSED_SIZE","filteredProps","maskChar"],"mappings":";AAAA,SAA0BA,WAAW,QAAQ,QAAQ;AACrD,OAAOC,oBAAoB,mBAAmB;AAE9C,SAASC,SAAS,QAAwB,uBAAuB;AACjE,SAAkBC,aAAa,QAAQ,wBAAwB;AAC/D,SAASC,uBAAuB,QAAQ,0BAA0B;AAOlE,OAAO,MAAMC,qBAAkDC,CAAAA;;IAC3D,MAAM,EACFC,KAAK,EACLC,QAAQ,EACRC,aAAaC,eAAe,EAC5BC,SAASC,WAAW,EACpBC,GAAG,EACHC,OAAO,EACPC,MAAM,EACNC,QAAQ,EACRC,QAAQ,EACR,GAAGC,WACN,GAAGZ;IAEJ,MAAM,CAACa,gBAAgB,GAAGf,wBAAwBD;IAClD,MAAMQ,UAAUC,wBAAAA,yBAAAA,cAAeO;IAC/B,MAAM,EAAEC,kBAAkB,aAAa,EAAEC,yBAAyB,EAAE,EAAE,WAClEV,oBAAAA,8BAAAA,QAASW,WAAW,uCAAI,CAAC;IAE7B,MAAMC,OAAOH,gBAAgBI,OAAO,CAAC,OAAO;IAC5C,MAAMf,cAAcC,4BAAAA,6BAAAA,kBAAmBW;IACvC,MAAMI,eAAeC,OAAOnB,kBAAAA,mBAAAA,QAAS,IAAIiB,OAAO,CAAC,OAAO;IAExD,mDAAmD;IACnD,MAAMG,eAAe3B,YACjB,CAAC4B;QACG,IAAIpB,UAAU;YACV,MAAMqB,aAAaD,MAAME,MAAM,CAACvB,KAAK,CAACiB,OAAO,CAAC,OAAO;YAErD,MAAMO,gBAAgB;gBAClB,GAAGH,KAAK;gBACRE,QAAQ;oBAAE,GAAGF,MAAME,MAAM;oBAAEvB,OAAOsB;gBAAW;gBAC7CG,eAAe;oBAAE,GAAGJ,MAAMI,aAAa;oBAAEzB,OAAOsB;gBAAW;YAC/D;YAEArB,SAASuB;QACb;IACJ,GACA;QAACvB;KAAS;IAGd,IAAIK,KAAK;QACL,qBACI,KAACX;YACI,GAAGgB,SAAS;YACbX,OAAOkB;YACPjB,UAAUmB;YACVb,SAASA;YACTC,QAAQA;YACRC,UAAUA;YACVC,UAAUA;YACVgB,MAAK;YACLxB,aAAaA;;IAGzB;IAEA,MAAMyB,kBAAkB,CAACC;QACrB,iEAAiE;QACjE,MAAM,EAAEC,MAAMC,WAAW,EAAE,GAAGC,eAAe,GAAGH;QAChD,qBAAO,KAACjC;YAAW,GAAGoC,aAAa;YAAG,GAAGpB,SAAS;YAAEe,MAAK;YAAMxB,aAAaA;;IAChF;IAEA,qBACI,KAACR;QACGM,OAAOkB;QACPjB,UAAUmB;QACVJ,MAAMA;QACNgB,UAAU;QACVzB,SAASA;QACTC,QAAQA;QACRC,UAAUA;QACVC,UAAUA;kBAETiB;;AAGb,EAAE"}
1
+ {"version":3,"sources":["../../src/phone-number-input/phone-number-input-a2.tsx"],"sourcesContent":["import { ChangeEvent, FC, useCallback } from 'react';\nimport ReactInputMask from 'react-input-mask';\n\nimport { TextField, TextFieldProps } from '@servicetitan/anvil2';\nimport { Culture, CULTURE_TOKEN } from '@servicetitan/culture';\nimport { useOptionalDependencies } from '@servicetitan/react-ioc';\n\ntype PhoneNumberInputA2Props = TextFieldProps & {\n sip?: boolean;\n culture?: Culture;\n};\n\nexport const PhoneNumberInputA2: FC<PhoneNumberInputA2Props> = props => {\n const {\n value,\n onChange,\n placeholder: propPlaceholder,\n culture: propCulture,\n sip,\n onFocus,\n onBlur,\n disabled,\n readOnly,\n ...restProps\n } = props;\n\n const [injectedCulture] = useOptionalDependencies(CULTURE_TOKEN);\n const culture = propCulture ?? injectedCulture;\n const { SimplePhoneMask = '?9999999999', SimplePhonePlaceholder = '' } =\n culture?.PhoneFormat ?? {};\n\n const mask = SimplePhoneMask.replace(/\\?/g, '');\n const placeholder = propPlaceholder ?? SimplePhonePlaceholder;\n const displayValue = String(value ?? '').replace(/\\D/g, '');\n\n // Transform output value to digits-only (unmasked)\n const handleChange = useCallback(\n (event: ChangeEvent<HTMLInputElement>) => {\n if (onChange) {\n const digitsOnly = event.target.value.replace(/\\D/g, '');\n\n const unmaskedEvent = {\n ...event,\n target: { ...event.target, value: digitsOnly },\n currentTarget: { ...event.currentTarget, value: digitsOnly },\n } as ChangeEvent<HTMLInputElement>;\n\n onChange(unmaskedEvent);\n }\n },\n [onChange]\n );\n\n if (sip) {\n return (\n <TextField\n {...restProps}\n value={value}\n onChange={onChange}\n onFocus={onFocus}\n onBlur={onBlur}\n disabled={disabled}\n readOnly={readOnly}\n type=\"text\"\n placeholder={propPlaceholder}\n />\n );\n }\n\n const renderTextField = (inputProps: Record<string, unknown>) => {\n // Exclude 'size' as it conflicts with TextField's size prop type\n const { size: UNUSED_SIZE, ...filteredProps } = inputProps;\n return <TextField {...filteredProps} {...restProps} type=\"tel\" placeholder={placeholder} />;\n };\n\n return (\n <ReactInputMask\n value={displayValue}\n onChange={handleChange}\n mask={mask}\n maskChar={null}\n onFocus={onFocus}\n onBlur={onBlur}\n disabled={disabled}\n readOnly={readOnly}\n >\n {renderTextField as any}\n </ReactInputMask>\n );\n};\n"],"names":["useCallback","ReactInputMask","TextField","CULTURE_TOKEN","useOptionalDependencies","PhoneNumberInputA2","props","value","onChange","placeholder","propPlaceholder","culture","propCulture","sip","onFocus","onBlur","disabled","readOnly","restProps","injectedCulture","SimplePhoneMask","SimplePhonePlaceholder","PhoneFormat","mask","replace","displayValue","String","handleChange","event","digitsOnly","target","unmaskedEvent","currentTarget","type","renderTextField","inputProps","size","UNUSED_SIZE","filteredProps","maskChar"],"mappings":";AAAA,SAA0BA,WAAW,QAAQ,QAAQ;AACrD,OAAOC,oBAAoB,mBAAmB;AAE9C,SAASC,SAAS,QAAwB,uBAAuB;AACjE,SAAkBC,aAAa,QAAQ,wBAAwB;AAC/D,SAASC,uBAAuB,QAAQ,0BAA0B;AAOlE,OAAO,MAAMC,qBAAkDC,CAAAA;;IAC3D,MAAM,EACFC,KAAK,EACLC,QAAQ,EACRC,aAAaC,eAAe,EAC5BC,SAASC,WAAW,EACpBC,GAAG,EACHC,OAAO,EACPC,MAAM,EACNC,QAAQ,EACRC,QAAQ,EACR,GAAGC,WACN,GAAGZ;IAEJ,MAAM,CAACa,gBAAgB,GAAGf,wBAAwBD;IAClD,MAAMQ,UAAUC,wBAAAA,yBAAAA,cAAeO;IAC/B,MAAM,EAAEC,kBAAkB,aAAa,EAAEC,yBAAyB,EAAE,EAAE,WAClEV,oBAAAA,8BAAAA,QAASW,WAAW,uCAAI,CAAC;IAE7B,MAAMC,OAAOH,gBAAgBI,OAAO,CAAC,OAAO;IAC5C,MAAMf,cAAcC,4BAAAA,6BAAAA,kBAAmBW;IACvC,MAAMI,eAAeC,OAAOnB,kBAAAA,mBAAAA,QAAS,IAAIiB,OAAO,CAAC,OAAO;IAExD,mDAAmD;IACnD,MAAMG,eAAe3B,YACjB,CAAC4B;QACG,IAAIpB,UAAU;YACV,MAAMqB,aAAaD,MAAME,MAAM,CAACvB,KAAK,CAACiB,OAAO,CAAC,OAAO;YAErD,MAAMO,gBAAgB;gBAClB,GAAGH,KAAK;gBACRE,QAAQ;oBAAE,GAAGF,MAAME,MAAM;oBAAEvB,OAAOsB;gBAAW;gBAC7CG,eAAe;oBAAE,GAAGJ,MAAMI,aAAa;oBAAEzB,OAAOsB;gBAAW;YAC/D;YAEArB,SAASuB;QACb;IACJ,GACA;QAACvB;KAAS;IAGd,IAAIK,KAAK;QACL,qBACI,KAACX;YACI,GAAGgB,SAAS;YACbX,OAAOA;YACPC,UAAUA;YACVM,SAASA;YACTC,QAAQA;YACRC,UAAUA;YACVC,UAAUA;YACVgB,MAAK;YACLxB,aAAaC;;IAGzB;IAEA,MAAMwB,kBAAkB,CAACC;QACrB,iEAAiE;QACjE,MAAM,EAAEC,MAAMC,WAAW,EAAE,GAAGC,eAAe,GAAGH;QAChD,qBAAO,KAACjC;YAAW,GAAGoC,aAAa;YAAG,GAAGpB,SAAS;YAAEe,MAAK;YAAMxB,aAAaA;;IAChF;IAEA,qBACI,KAACR;QACGM,OAAOkB;QACPjB,UAAUmB;QACVJ,MAAMA;QACNgB,UAAU;QACVzB,SAASA;QACTC,QAAQA;QACRC,UAAUA;QACVC,UAAUA;kBAETiB;;AAGb,EAAE"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@servicetitan/form",
3
- "version": "38.6.0",
3
+ "version": "38.6.1",
4
4
  "description": "",
5
5
  "homepage": "https://docs.st.dev/docs/frontend/form",
6
6
  "repository": {
@@ -18,14 +18,16 @@
18
18
  "devDependencies": {
19
19
  "@progress/kendo-react-dateinputs": "~5.5.0",
20
20
  "@servicetitan/anvil2": "^2.0.1",
21
- "@servicetitan/confirm": "^38.6.0",
22
- "@servicetitan/culture": "^38.6.0",
21
+ "@servicetitan/confirm": "^38.6.1",
22
+ "@servicetitan/culture": "^38.6.1",
23
23
  "@servicetitan/design-system": "~14.5.1",
24
- "@servicetitan/form-state": "^38.6.0",
24
+ "@servicetitan/form-state": "^38.6.1",
25
25
  "@servicetitan/hash-browser-router": "^34.2.0",
26
26
  "@servicetitan/react-ioc": "^34.2.0",
27
27
  "@servicetitan/tokens": ">=12.2.1",
28
28
  "@servicetitan/web-components": "^34.2.0",
29
+ "@testing-library/jest-dom": "^6.4.2",
30
+ "@testing-library/react": "^14.2.1",
29
31
  "@types/js-cookie": "^3.0.3",
30
32
  "@types/react": "~18.2.55",
31
33
  "@types/react-input-mask": "~2.0.5",
@@ -41,10 +43,10 @@
41
43
  "peerDependencies": {
42
44
  "@progress/kendo-react-dateinputs": "~5.5.0",
43
45
  "@servicetitan/anvil2": ">=1.42.0",
44
- "@servicetitan/confirm": "^38.6.0",
45
- "@servicetitan/culture": "^38.6.0",
46
+ "@servicetitan/confirm": "^38.6.1",
47
+ "@servicetitan/culture": "^38.6.1",
46
48
  "@servicetitan/design-system": ">=13.2.1",
47
- "@servicetitan/form-state": "^38.6.0",
49
+ "@servicetitan/form-state": "^38.6.1",
48
50
  "@servicetitan/react-ioc": ">21.0.0",
49
51
  "@servicetitan/tokens": ">=12.2.1",
50
52
  "accounting": "~0.4.1",
@@ -69,5 +71,5 @@
69
71
  "less": true,
70
72
  "webpack": false
71
73
  },
72
- "gitHead": "b9bcab08eab9166d77e36cc6473321f977962f12"
74
+ "gitHead": "fcd96d8dd56219340083a3c8a181c10a81da48ae"
73
75
  }
@@ -0,0 +1,305 @@
1
+ import { render, screen, fireEvent } from '@testing-library/react';
2
+ import '@testing-library/jest-dom';
3
+
4
+ import { Culture } from '@servicetitan/culture';
5
+
6
+ // Mock useOptionalDependencies from react-ioc
7
+ const mockUseOptionalDependencies = jest.fn();
8
+ jest.mock('@servicetitan/react-ioc', () => ({
9
+ ...jest.requireActual('@servicetitan/react-ioc'),
10
+ useOptionalDependencies: (...args: any[]) => mockUseOptionalDependencies(...args),
11
+ }));
12
+
13
+ // Mock TextField as a plain <input> to enable testing with testing-library
14
+ jest.mock('@servicetitan/anvil2', () => {
15
+ const { createElement, forwardRef } = jest.requireActual('react');
16
+ return {
17
+ TextField: forwardRef(
18
+ (
19
+ {
20
+ label,
21
+ error,
22
+ suffix,
23
+ prefix,
24
+ description,
25
+ hint,
26
+ loading,
27
+ moreInfo,
28
+ labelProps,
29
+ errorAriaLive,
30
+ maxLengthCounter,
31
+ ...props
32
+ }: any,
33
+ ref: any
34
+ ) => createElement('input', { ...props, ref, 'aria-label': label ?? 'phone' })
35
+ ),
36
+ };
37
+ });
38
+
39
+ // Mock react-input-mask: render children with mask-related props
40
+ jest.mock('react-input-mask', () => {
41
+ const { createElement } = jest.requireActual('react');
42
+ const mod: Record<string, unknown> = {
43
+ default: ({ children, mask, value, onChange, maskChar, ...rest }: any) => {
44
+ const inputProps = { value, onChange, 'data-mask': mask, ...rest };
45
+ if (typeof children === 'function') {
46
+ return children(inputProps);
47
+ }
48
+ return createElement('input', inputProps);
49
+ },
50
+ };
51
+ // eslint-disable-next-line @typescript-eslint/naming-convention
52
+ mod.__esModule = true;
53
+ return mod;
54
+ });
55
+
56
+ import { PhoneNumberInputA2 } from '../phone-number-input-a2';
57
+
58
+ const nanpCulture = {
59
+ PhoneFormat: {
60
+ SimplePhoneMask: '(999) 999-9999?',
61
+ SimplePhonePlaceholder: '(___) ___-____',
62
+ },
63
+ } as Culture;
64
+
65
+ const auCulture = {
66
+ PhoneFormat: {
67
+ SimplePhoneMask: '99 9999 9999',
68
+ SimplePhonePlaceholder: '__ ____ ____',
69
+ },
70
+ } as Culture;
71
+
72
+ describe('PhoneNumberInputA2', () => {
73
+ beforeEach(() => {
74
+ jest.clearAllMocks();
75
+ mockUseOptionalDependencies.mockReturnValue([undefined]);
76
+ });
77
+
78
+ describe('culture resolution', () => {
79
+ it('uses explicit culture prop over DI culture', () => {
80
+ mockUseOptionalDependencies.mockReturnValue([auCulture]);
81
+
82
+ render(<PhoneNumberInputA2 culture={nanpCulture} value="" onChange={jest.fn()} />);
83
+
84
+ const input = screen.getByRole('textbox');
85
+ expect(input).toHaveAttribute('placeholder', '(___) ___-____');
86
+ });
87
+
88
+ it('uses DI culture when no culture prop is provided', () => {
89
+ mockUseOptionalDependencies.mockReturnValue([auCulture]);
90
+
91
+ render(<PhoneNumberInputA2 value="" onChange={jest.fn()} />);
92
+
93
+ const input = screen.getByRole('textbox');
94
+ expect(input).toHaveAttribute('placeholder', '__ ____ ____');
95
+ });
96
+
97
+ it('uses fallback mask when no culture is available', () => {
98
+ mockUseOptionalDependencies.mockReturnValue([undefined]);
99
+
100
+ render(<PhoneNumberInputA2 value="" onChange={jest.fn()} />);
101
+
102
+ const input = screen.getByRole('textbox');
103
+ // Fallback mask is '?9999999999' → stripped to '9999999999'
104
+ expect(input).toHaveAttribute('data-mask', '9999999999');
105
+ });
106
+ });
107
+
108
+ describe('placeholder', () => {
109
+ it('uses explicit placeholder prop over culture placeholder', () => {
110
+ render(
111
+ <PhoneNumberInputA2
112
+ culture={nanpCulture}
113
+ placeholder="Enter phone"
114
+ value=""
115
+ onChange={jest.fn()}
116
+ />
117
+ );
118
+
119
+ const input = screen.getByRole('textbox');
120
+ expect(input).toHaveAttribute('placeholder', 'Enter phone');
121
+ });
122
+ });
123
+
124
+ describe('SIP mode', () => {
125
+ it('renders non-digit characters in value (no masking)', () => {
126
+ render(<PhoneNumberInputA2 sip value="sip:user@domain.com" onChange={jest.fn()} />);
127
+
128
+ const input = screen.getByRole('textbox');
129
+ // In SIP mode, value is passed through as-is (not stripped to digits)
130
+ expect(input).toHaveValue('sip:user@domain.com');
131
+ });
132
+
133
+ it('passes onChange directly without digit stripping', () => {
134
+ const handleChange = jest.fn();
135
+
136
+ render(<PhoneNumberInputA2 sip value="" onChange={handleChange} />);
137
+
138
+ const input = screen.getByRole('textbox');
139
+ fireEvent.change(input, { target: { value: 'sip:test@host' } });
140
+
141
+ expect(handleChange).toHaveBeenCalledTimes(1);
142
+ /*
143
+ * In SIP mode, onChange is the raw prop (not the digit-stripping wrapper).
144
+ * The event's target should be the actual DOM input element, not a
145
+ * plain object spread (which is what handleChange produces in non-SIP).
146
+ */
147
+ const event = handleChange.mock.calls[0][0];
148
+ expect(event.target).toBe(input);
149
+ });
150
+
151
+ it('renders input with type="text"', () => {
152
+ render(<PhoneNumberInputA2 sip value="" onChange={jest.fn()} />);
153
+
154
+ const input = screen.getByRole('textbox');
155
+ expect(input).toHaveAttribute('type', 'text');
156
+ });
157
+
158
+ it('does not show culture placeholder', () => {
159
+ render(<PhoneNumberInputA2 sip culture={nanpCulture} value="" onChange={jest.fn()} />);
160
+
161
+ const input = screen.getByRole('textbox');
162
+ expect(input).not.toHaveAttribute('placeholder', '(___) ___-____');
163
+ });
164
+
165
+ it('shows explicit placeholder even in SIP mode', () => {
166
+ render(
167
+ <PhoneNumberInputA2
168
+ sip
169
+ culture={nanpCulture}
170
+ placeholder="SIP address"
171
+ value=""
172
+ onChange={jest.fn()}
173
+ />
174
+ );
175
+
176
+ const input = screen.getByRole('textbox');
177
+ expect(input).toHaveAttribute('placeholder', 'SIP address');
178
+ });
179
+ });
180
+
181
+ describe('masked (non-SIP) mode', () => {
182
+ it('renders with ReactInputMask', () => {
183
+ render(<PhoneNumberInputA2 culture={nanpCulture} value="" onChange={jest.fn()} />);
184
+
185
+ const input = screen.getByRole('textbox');
186
+ // Our mock passes mask as data-mask attribute
187
+ expect(input).toHaveAttribute('data-mask', '(999) 999-9999');
188
+ });
189
+
190
+ it('strips non-digits from onChange value', () => {
191
+ const handleChange = jest.fn();
192
+
193
+ render(<PhoneNumberInputA2 culture={nanpCulture} value="" onChange={handleChange} />);
194
+
195
+ const input = screen.getByRole('textbox');
196
+ fireEvent.change(input, {
197
+ target: { value: '(555) 123-4567' },
198
+ currentTarget: { value: '(555) 123-4567' },
199
+ });
200
+
201
+ expect(handleChange).toHaveBeenCalledTimes(1);
202
+ const event = handleChange.mock.calls[0][0];
203
+ expect(event.target.value).toBe('5551234567');
204
+ expect(event.currentTarget.value).toBe('5551234567');
205
+ });
206
+
207
+ it('renders input with type="tel"', () => {
208
+ render(<PhoneNumberInputA2 culture={nanpCulture} value="" onChange={jest.fn()} />);
209
+
210
+ const input = screen.getByRole('textbox');
211
+ expect(input).toHaveAttribute('type', 'tel');
212
+ });
213
+
214
+ it('displays digits-only value', () => {
215
+ render(
216
+ <PhoneNumberInputA2 culture={nanpCulture} value="5551234567" onChange={jest.fn()} />
217
+ );
218
+
219
+ const input = screen.getByRole('textbox');
220
+ expect(input).toHaveValue('5551234567');
221
+ });
222
+
223
+ it('strips question mark from mask', () => {
224
+ const cultureWithOptional = {
225
+ PhoneFormat: {
226
+ SimplePhoneMask: '(999) 999-9999?',
227
+ SimplePhonePlaceholder: '',
228
+ },
229
+ } as Culture;
230
+
231
+ render(
232
+ <PhoneNumberInputA2 culture={cultureWithOptional} value="" onChange={jest.fn()} />
233
+ );
234
+
235
+ const input = screen.getByRole('textbox');
236
+ expect(input).toHaveAttribute('data-mask', '(999) 999-9999');
237
+ });
238
+ });
239
+
240
+ describe('props forwarding', () => {
241
+ it('forwards disabled, readOnly, onFocus, onBlur in non-SIP mode', () => {
242
+ const onFocus = jest.fn();
243
+ const onBlur = jest.fn();
244
+
245
+ render(
246
+ <PhoneNumberInputA2
247
+ culture={nanpCulture}
248
+ value=""
249
+ onChange={jest.fn()}
250
+ disabled
251
+ readOnly
252
+ onFocus={onFocus}
253
+ onBlur={onBlur}
254
+ />
255
+ );
256
+
257
+ const input = screen.getByRole('textbox');
258
+ expect(input).toBeDisabled();
259
+ expect(input).toHaveAttribute('readOnly');
260
+
261
+ fireEvent.focus(input);
262
+ expect(onFocus).toHaveBeenCalledTimes(1);
263
+
264
+ fireEvent.blur(input);
265
+ expect(onBlur).toHaveBeenCalledTimes(1);
266
+ });
267
+
268
+ it('forwards disabled, readOnly, onFocus, onBlur in SIP mode', () => {
269
+ const onFocus = jest.fn();
270
+ const onBlur = jest.fn();
271
+
272
+ render(
273
+ <PhoneNumberInputA2
274
+ sip
275
+ value=""
276
+ onChange={jest.fn()}
277
+ disabled
278
+ readOnly
279
+ onFocus={onFocus}
280
+ onBlur={onBlur}
281
+ />
282
+ );
283
+
284
+ const input = screen.getByRole('textbox');
285
+ expect(input).toBeDisabled();
286
+ expect(input).toHaveAttribute('readOnly');
287
+
288
+ fireEvent.focus(input);
289
+ expect(onFocus).toHaveBeenCalledTimes(1);
290
+
291
+ fireEvent.blur(input);
292
+ expect(onBlur).toHaveBeenCalledTimes(1);
293
+ });
294
+
295
+ it('does not call onChange when onChange is not provided', () => {
296
+ // Should not throw
297
+ render(<PhoneNumberInputA2 culture={nanpCulture} value="" />);
298
+
299
+ const input = screen.getByRole('textbox');
300
+ expect(() => {
301
+ fireEvent.change(input, { target: { value: '123' } });
302
+ }).not.toThrow();
303
+ });
304
+ });
305
+ });
@@ -55,14 +55,14 @@ export const PhoneNumberInputA2: FC<PhoneNumberInputA2Props> = props => {
55
55
  return (
56
56
  <TextField
57
57
  {...restProps}
58
- value={displayValue}
59
- onChange={handleChange}
58
+ value={value}
59
+ onChange={onChange}
60
60
  onFocus={onFocus}
61
61
  onBlur={onBlur}
62
62
  disabled={disabled}
63
63
  readOnly={readOnly}
64
- type="tel"
65
- placeholder={placeholder}
64
+ type="text"
65
+ placeholder={propPlaceholder}
66
66
  />
67
67
  );
68
68
  }