@servicetitan/form 38.6.0 → 38.7.0

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.
@@ -6,13 +6,13 @@ const cultures = {
6
6
  nanp: {
7
7
  PhoneFormat: {
8
8
  SimplePhoneMask: '(999) 999-9999?',
9
- SimplePhonePlaceholder: '(___) ___-____'
9
+ SampleSimplePhoneNumber: '(123) 456-7890'
10
10
  }
11
11
  },
12
12
  au: {
13
13
  PhoneFormat: {
14
14
  SimplePhoneMask: '99 9999 9999',
15
- SimplePhonePlaceholder: '__ ____ ____'
15
+ SampleSimplePhoneNumber: '04 1234 5678'
16
16
  }
17
17
  }
18
18
  };
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/demo/phone-number-input-a2.tsx"],"sourcesContent":["import { ChangeEvent, FC, useState } from 'react';\n\nimport { observer } from 'mobx-react';\n\nimport { Culture } from '@servicetitan/culture';\n\nimport { PhoneNumberInputA2 } from '..';\n\ntype CultureKey = 'nanp' | 'au';\n\nconst cultures: Record<CultureKey, Culture> = {\n nanp: {\n PhoneFormat: {\n SimplePhoneMask: '(999) 999-9999?',\n SimplePhonePlaceholder: '(___) ___-____',\n },\n } as Culture,\n au: {\n PhoneFormat: {\n SimplePhoneMask: '99 9999 9999',\n SimplePhonePlaceholder: '__ ____ ____',\n },\n } as Culture,\n};\n\nexport const PhoneNumberInputA2Example: FC = observer(() => {\n const [value, setValue] = useState('');\n const [cultureKey, setCultureKey] = useState<CultureKey>('nanp');\n const [sip, setSip] = useState(false);\n\n const handleChange = (e: ChangeEvent<HTMLInputElement>) => {\n setValue(e.target.value);\n };\n\n const handleCultureChange = (e: ChangeEvent<HTMLInputElement>) => {\n setCultureKey(e.target.value as CultureKey);\n setValue('');\n };\n\n return (\n <div>\n <fieldset>\n <legend>Region</legend>\n <label>\n <input\n type=\"radio\"\n name=\"culture\"\n value=\"nanp\"\n checked={cultureKey === 'nanp'}\n onChange={handleCultureChange}\n />\n US/Canada (NANP)\n </label>\n <label style={{ marginLeft: 16 }}>\n <input\n type=\"radio\"\n name=\"culture\"\n value=\"au\"\n checked={cultureKey === 'au'}\n onChange={handleCultureChange}\n />\n Australia\n </label>\n </fieldset>\n <br />\n <label>\n <input\n type=\"checkbox\"\n checked={sip}\n onChange={e => {\n setSip(e.target.checked);\n setValue('');\n }}\n />\n SIP\n </label>\n <br />\n <br />\n <PhoneNumberInputA2\n culture={cultures[cultureKey]}\n label=\"Phone\"\n value={value}\n onChange={handleChange}\n sip={sip}\n />\n <p>Returned value: {value}</p>\n </div>\n );\n});\n"],"names":["useState","observer","PhoneNumberInputA2","cultures","nanp","PhoneFormat","SimplePhoneMask","SimplePhonePlaceholder","au","PhoneNumberInputA2Example","value","setValue","cultureKey","setCultureKey","sip","setSip","handleChange","e","target","handleCultureChange","div","fieldset","legend","label","input","type","name","checked","onChange","style","marginLeft","br","culture","p"],"mappings":";AAAA,SAA0BA,QAAQ,QAAQ,QAAQ;AAElD,SAASC,QAAQ,QAAQ,aAAa;AAItC,SAASC,kBAAkB,QAAQ,KAAK;AAIxC,MAAMC,WAAwC;IAC1CC,MAAM;QACFC,aAAa;YACTC,iBAAiB;YACjBC,wBAAwB;QAC5B;IACJ;IACAC,IAAI;QACAH,aAAa;YACTC,iBAAiB;YACjBC,wBAAwB;QAC5B;IACJ;AACJ;AAEA,OAAO,MAAME,4BAAgCR,SAAS;IAClD,MAAM,CAACS,OAAOC,SAAS,GAAGX,SAAS;IACnC,MAAM,CAACY,YAAYC,cAAc,GAAGb,SAAqB;IACzD,MAAM,CAACc,KAAKC,OAAO,GAAGf,SAAS;IAE/B,MAAMgB,eAAe,CAACC;QAClBN,SAASM,EAAEC,MAAM,CAACR,KAAK;IAC3B;IAEA,MAAMS,sBAAsB,CAACF;QACzBJ,cAAcI,EAAEC,MAAM,CAACR,KAAK;QAC5BC,SAAS;IACb;IAEA,qBACI,MAACS;;0BACG,MAACC;;kCACG,KAACC;kCAAO;;kCACR,MAACC;;0CACG,KAACC;gCACGC,MAAK;gCACLC,MAAK;gCACLhB,OAAM;gCACNiB,SAASf,eAAe;gCACxBgB,UAAUT;;4BACZ;;;kCAGN,MAACI;wBAAMM,OAAO;4BAAEC,YAAY;wBAAG;;0CAC3B,KAACN;gCACGC,MAAK;gCACLC,MAAK;gCACLhB,OAAM;gCACNiB,SAASf,eAAe;gCACxBgB,UAAUT;;4BACZ;;;;;0BAIV,KAACY;0BACD,MAACR;;kCACG,KAACC;wBACGC,MAAK;wBACLE,SAASb;wBACTc,UAAUX,CAAAA;4BACNF,OAAOE,EAAEC,MAAM,CAACS,OAAO;4BACvBhB,SAAS;wBACb;;oBACF;;;0BAGN,KAACoB;0BACD,KAACA;0BACD,KAAC7B;gBACG8B,SAAS7B,QAAQ,CAACS,WAAW;gBAC7BW,OAAM;gBACNb,OAAOA;gBACPkB,UAAUZ;gBACVF,KAAKA;;0BAET,MAACmB;;oBAAE;oBAAiBvB;;;;;AAGhC,GAAG"}
1
+ {"version":3,"sources":["../../src/demo/phone-number-input-a2.tsx"],"sourcesContent":["import { ChangeEvent, FC, useState } from 'react';\n\nimport { observer } from 'mobx-react';\n\nimport { Culture } from '@servicetitan/culture';\n\nimport { PhoneNumberInputA2 } from '..';\n\ntype CultureKey = 'nanp' | 'au';\n\nconst cultures: Record<CultureKey, Culture> = {\n nanp: {\n PhoneFormat: {\n SimplePhoneMask: '(999) 999-9999?',\n SampleSimplePhoneNumber: '(123) 456-7890',\n },\n } as Culture,\n au: {\n PhoneFormat: {\n SimplePhoneMask: '99 9999 9999',\n SampleSimplePhoneNumber: '04 1234 5678',\n },\n } as Culture,\n};\n\nexport const PhoneNumberInputA2Example: FC = observer(() => {\n const [value, setValue] = useState('');\n const [cultureKey, setCultureKey] = useState<CultureKey>('nanp');\n const [sip, setSip] = useState(false);\n\n const handleChange = (e: ChangeEvent<HTMLInputElement>) => {\n setValue(e.target.value);\n };\n\n const handleCultureChange = (e: ChangeEvent<HTMLInputElement>) => {\n setCultureKey(e.target.value as CultureKey);\n setValue('');\n };\n\n return (\n <div>\n <fieldset>\n <legend>Region</legend>\n <label>\n <input\n type=\"radio\"\n name=\"culture\"\n value=\"nanp\"\n checked={cultureKey === 'nanp'}\n onChange={handleCultureChange}\n />\n US/Canada (NANP)\n </label>\n <label style={{ marginLeft: 16 }}>\n <input\n type=\"radio\"\n name=\"culture\"\n value=\"au\"\n checked={cultureKey === 'au'}\n onChange={handleCultureChange}\n />\n Australia\n </label>\n </fieldset>\n <br />\n <label>\n <input\n type=\"checkbox\"\n checked={sip}\n onChange={e => {\n setSip(e.target.checked);\n setValue('');\n }}\n />\n SIP\n </label>\n <br />\n <br />\n <PhoneNumberInputA2\n culture={cultures[cultureKey]}\n label=\"Phone\"\n value={value}\n onChange={handleChange}\n sip={sip}\n />\n <p>Returned value: {value}</p>\n </div>\n );\n});\n"],"names":["useState","observer","PhoneNumberInputA2","cultures","nanp","PhoneFormat","SimplePhoneMask","SampleSimplePhoneNumber","au","PhoneNumberInputA2Example","value","setValue","cultureKey","setCultureKey","sip","setSip","handleChange","e","target","handleCultureChange","div","fieldset","legend","label","input","type","name","checked","onChange","style","marginLeft","br","culture","p"],"mappings":";AAAA,SAA0BA,QAAQ,QAAQ,QAAQ;AAElD,SAASC,QAAQ,QAAQ,aAAa;AAItC,SAASC,kBAAkB,QAAQ,KAAK;AAIxC,MAAMC,WAAwC;IAC1CC,MAAM;QACFC,aAAa;YACTC,iBAAiB;YACjBC,yBAAyB;QAC7B;IACJ;IACAC,IAAI;QACAH,aAAa;YACTC,iBAAiB;YACjBC,yBAAyB;QAC7B;IACJ;AACJ;AAEA,OAAO,MAAME,4BAAgCR,SAAS;IAClD,MAAM,CAACS,OAAOC,SAAS,GAAGX,SAAS;IACnC,MAAM,CAACY,YAAYC,cAAc,GAAGb,SAAqB;IACzD,MAAM,CAACc,KAAKC,OAAO,GAAGf,SAAS;IAE/B,MAAMgB,eAAe,CAACC;QAClBN,SAASM,EAAEC,MAAM,CAACR,KAAK;IAC3B;IAEA,MAAMS,sBAAsB,CAACF;QACzBJ,cAAcI,EAAEC,MAAM,CAACR,KAAK;QAC5BC,SAAS;IACb;IAEA,qBACI,MAACS;;0BACG,MAACC;;kCACG,KAACC;kCAAO;;kCACR,MAACC;;0CACG,KAACC;gCACGC,MAAK;gCACLC,MAAK;gCACLhB,OAAM;gCACNiB,SAASf,eAAe;gCACxBgB,UAAUT;;4BACZ;;;kCAGN,MAACI;wBAAMM,OAAO;4BAAEC,YAAY;wBAAG;;0CAC3B,KAACN;gCACGC,MAAK;gCACLC,MAAK;gCACLhB,OAAM;gCACNiB,SAASf,eAAe;gCACxBgB,UAAUT;;4BACZ;;;;;0BAIV,KAACY;0BACD,MAACR;;kCACG,KAACC;wBACGC,MAAK;wBACLE,SAASb;wBACTc,UAAUX,CAAAA;4BACNF,OAAOE,EAAEC,MAAM,CAACS,OAAO;4BACvBhB,SAAS;wBACb;;oBACF;;;0BAGN,KAACoB;0BACD,KAACA;0BACD,KAAC7B;gBACG8B,SAAS7B,QAAQ,CAACS,WAAW;gBAC7BW,OAAM;gBACNb,OAAOA;gBACPkB,UAAUZ;gBACVF,KAAKA;;0BAET,MAACmB;;oBAAE;oBAAiBvB;;;;;AAGhC,GAAG"}
@@ -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"}
@@ -9,9 +9,9 @@ export const PhoneNumberInputA2 = (props)=>{
9
9
  const { value, onChange, placeholder: propPlaceholder, culture: propCulture, sip, onFocus, onBlur, disabled, readOnly, ...restProps } = props;
10
10
  const [injectedCulture] = useOptionalDependencies(CULTURE_TOKEN);
11
11
  const culture = propCulture !== null && propCulture !== void 0 ? propCulture : injectedCulture;
12
- const { SimplePhoneMask = '?9999999999', SimplePhonePlaceholder = '' } = (_ref = culture === null || culture === void 0 ? void 0 : culture.PhoneFormat) !== null && _ref !== void 0 ? _ref : {};
12
+ const { SimplePhoneMask = '?9999999999', SampleSimplePhoneNumber = '' } = (_ref = culture === null || culture === void 0 ? void 0 : culture.PhoneFormat) !== null && _ref !== void 0 ? _ref : {};
13
13
  const mask = SimplePhoneMask.replace(/\?/g, '');
14
- const placeholder = propPlaceholder !== null && propPlaceholder !== void 0 ? propPlaceholder : SimplePhonePlaceholder;
14
+ const placeholder = propPlaceholder !== null && propPlaceholder !== void 0 ? propPlaceholder : SampleSimplePhoneNumber;
15
15
  const displayValue = String(value !== null && value !== void 0 ? value : '').replace(/\D/g, '');
16
16
  // Transform output value to digits-only (unmasked)
17
17
  const handleChange = useCallback((event)=>{
@@ -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', SampleSimplePhoneNumber = '' } =\n culture?.PhoneFormat ?? {};\n\n const mask = SimplePhoneMask.replace(/\\?/g, '');\n const placeholder = propPlaceholder ?? SampleSimplePhoneNumber;\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","SampleSimplePhoneNumber","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,0BAA0B,EAAE,EAAE,WACnEV,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.7.0",
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.7.0",
22
+ "@servicetitan/culture": "^38.7.0",
23
23
  "@servicetitan/design-system": "~14.5.1",
24
- "@servicetitan/form-state": "^38.6.0",
24
+ "@servicetitan/form-state": "^38.7.0",
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.7.0",
47
+ "@servicetitan/culture": "^38.7.0",
46
48
  "@servicetitan/design-system": ">=13.2.1",
47
- "@servicetitan/form-state": "^38.6.0",
49
+ "@servicetitan/form-state": "^38.7.0",
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": "294a6fa2cafd00b82d6a7e0f5f9bea6acab9b2b6"
73
75
  }
@@ -12,13 +12,13 @@ const cultures: Record<CultureKey, Culture> = {
12
12
  nanp: {
13
13
  PhoneFormat: {
14
14
  SimplePhoneMask: '(999) 999-9999?',
15
- SimplePhonePlaceholder: '(___) ___-____',
15
+ SampleSimplePhoneNumber: '(123) 456-7890',
16
16
  },
17
17
  } as Culture,
18
18
  au: {
19
19
  PhoneFormat: {
20
20
  SimplePhoneMask: '99 9999 9999',
21
- SimplePhonePlaceholder: '__ ____ ____',
21
+ SampleSimplePhoneNumber: '04 1234 5678',
22
22
  },
23
23
  } as Culture,
24
24
  };
@@ -0,0 +1,324 @@
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
+ mod.__esModule = true;
52
+ return mod;
53
+ });
54
+
55
+ import { PhoneNumberInputA2 } from '../phone-number-input-a2';
56
+
57
+ const nanpCulture = {
58
+ PhoneFormat: {
59
+ SimplePhoneMask: '(999) 999-9999?',
60
+ SampleSimplePhoneNumber: '(123) 456-7890',
61
+ },
62
+ } as Culture;
63
+
64
+ const auCulture = {
65
+ PhoneFormat: {
66
+ SimplePhoneMask: '99 9999 9999',
67
+ SampleSimplePhoneNumber: '04 1234 5678',
68
+ },
69
+ } as Culture;
70
+
71
+ describe('PhoneNumberInputA2', () => {
72
+ beforeEach(() => {
73
+ jest.clearAllMocks();
74
+ mockUseOptionalDependencies.mockReturnValue([undefined]);
75
+ });
76
+
77
+ describe('culture resolution', () => {
78
+ it('uses explicit culture prop over DI culture', () => {
79
+ mockUseOptionalDependencies.mockReturnValue([auCulture]);
80
+
81
+ render(<PhoneNumberInputA2 culture={nanpCulture} value="" onChange={jest.fn()} />);
82
+
83
+ const input = screen.getByRole('textbox');
84
+ expect(input).toHaveAttribute('placeholder', '(123) 456-7890');
85
+ });
86
+
87
+ it('uses DI culture when no culture prop is provided', () => {
88
+ mockUseOptionalDependencies.mockReturnValue([auCulture]);
89
+
90
+ render(<PhoneNumberInputA2 value="" onChange={jest.fn()} />);
91
+
92
+ const input = screen.getByRole('textbox');
93
+ expect(input).toHaveAttribute('placeholder', '04 1234 5678');
94
+ });
95
+
96
+ it('uses fallback mask when no culture is available', () => {
97
+ mockUseOptionalDependencies.mockReturnValue([undefined]);
98
+
99
+ render(<PhoneNumberInputA2 value="" onChange={jest.fn()} />);
100
+
101
+ const input = screen.getByRole('textbox');
102
+ // Fallback mask is '?9999999999' → stripped to '9999999999'
103
+ expect(input).toHaveAttribute('data-mask', '9999999999');
104
+ });
105
+ });
106
+
107
+ describe('placeholder', () => {
108
+ it('renders empty placeholder when SampleSimplePhoneNumber is empty', () => {
109
+ const cultureWithEmptySample = {
110
+ PhoneFormat: {
111
+ SimplePhoneMask: '(999) 999-9999?',
112
+ SampleSimplePhoneNumber: '',
113
+ },
114
+ } as Culture;
115
+
116
+ render(
117
+ <PhoneNumberInputA2
118
+ culture={cultureWithEmptySample}
119
+ value=""
120
+ onChange={jest.fn()}
121
+ />
122
+ );
123
+
124
+ const input = screen.getByRole('textbox');
125
+ expect(input).toHaveAttribute('placeholder', '');
126
+ });
127
+
128
+ it('uses explicit placeholder prop over culture sample phone number', () => {
129
+ render(
130
+ <PhoneNumberInputA2
131
+ culture={nanpCulture}
132
+ placeholder="Enter phone"
133
+ value=""
134
+ onChange={jest.fn()}
135
+ />
136
+ );
137
+
138
+ const input = screen.getByRole('textbox');
139
+ expect(input).toHaveAttribute('placeholder', 'Enter phone');
140
+ });
141
+ });
142
+
143
+ describe('SIP mode', () => {
144
+ it('renders non-digit characters in value (no masking)', () => {
145
+ render(<PhoneNumberInputA2 sip value="sip:user@domain.com" onChange={jest.fn()} />);
146
+
147
+ const input = screen.getByRole('textbox');
148
+ // In SIP mode, value is passed through as-is (not stripped to digits)
149
+ expect(input).toHaveValue('sip:user@domain.com');
150
+ });
151
+
152
+ it('passes onChange directly without digit stripping', () => {
153
+ const handleChange = jest.fn();
154
+
155
+ render(<PhoneNumberInputA2 sip value="" onChange={handleChange} />);
156
+
157
+ const input = screen.getByRole('textbox');
158
+ fireEvent.change(input, { target: { value: 'sip:test@host' } });
159
+
160
+ expect(handleChange).toHaveBeenCalledTimes(1);
161
+ /*
162
+ * In SIP mode, onChange is the raw prop (not the digit-stripping wrapper).
163
+ * The event's target should be the actual DOM input element, not a
164
+ * plain object spread (which is what handleChange produces in non-SIP).
165
+ */
166
+ const event = handleChange.mock.calls[0][0];
167
+ expect(event.target).toBe(input);
168
+ });
169
+
170
+ it('renders input with type="text"', () => {
171
+ render(<PhoneNumberInputA2 sip value="" onChange={jest.fn()} />);
172
+
173
+ const input = screen.getByRole('textbox');
174
+ expect(input).toHaveAttribute('type', 'text');
175
+ });
176
+
177
+ it('does not show culture placeholder', () => {
178
+ render(<PhoneNumberInputA2 sip culture={nanpCulture} value="" onChange={jest.fn()} />);
179
+
180
+ const input = screen.getByRole('textbox');
181
+ expect(input).not.toHaveAttribute('placeholder', '(123) 456-7890');
182
+ });
183
+
184
+ it('shows explicit placeholder even in SIP mode', () => {
185
+ render(
186
+ <PhoneNumberInputA2
187
+ sip
188
+ culture={nanpCulture}
189
+ placeholder="SIP address"
190
+ value=""
191
+ onChange={jest.fn()}
192
+ />
193
+ );
194
+
195
+ const input = screen.getByRole('textbox');
196
+ expect(input).toHaveAttribute('placeholder', 'SIP address');
197
+ });
198
+ });
199
+
200
+ describe('masked (non-SIP) mode', () => {
201
+ it('renders with ReactInputMask', () => {
202
+ render(<PhoneNumberInputA2 culture={nanpCulture} value="" onChange={jest.fn()} />);
203
+
204
+ const input = screen.getByRole('textbox');
205
+ // Our mock passes mask as data-mask attribute
206
+ expect(input).toHaveAttribute('data-mask', '(999) 999-9999');
207
+ });
208
+
209
+ it('strips non-digits from onChange value', () => {
210
+ const handleChange = jest.fn();
211
+
212
+ render(<PhoneNumberInputA2 culture={nanpCulture} value="" onChange={handleChange} />);
213
+
214
+ const input = screen.getByRole('textbox');
215
+ fireEvent.change(input, {
216
+ target: { value: '(555) 123-4567' },
217
+ currentTarget: { value: '(555) 123-4567' },
218
+ });
219
+
220
+ expect(handleChange).toHaveBeenCalledTimes(1);
221
+ const event = handleChange.mock.calls[0][0];
222
+ expect(event.target.value).toBe('5551234567');
223
+ expect(event.currentTarget.value).toBe('5551234567');
224
+ });
225
+
226
+ it('renders input with type="tel"', () => {
227
+ render(<PhoneNumberInputA2 culture={nanpCulture} value="" onChange={jest.fn()} />);
228
+
229
+ const input = screen.getByRole('textbox');
230
+ expect(input).toHaveAttribute('type', 'tel');
231
+ });
232
+
233
+ it('displays digits-only value', () => {
234
+ render(
235
+ <PhoneNumberInputA2 culture={nanpCulture} value="5551234567" onChange={jest.fn()} />
236
+ );
237
+
238
+ const input = screen.getByRole('textbox');
239
+ expect(input).toHaveValue('5551234567');
240
+ });
241
+
242
+ it('strips question mark from mask', () => {
243
+ const cultureWithOptional = {
244
+ PhoneFormat: {
245
+ SimplePhoneMask: '(999) 999-9999?',
246
+ SampleSimplePhoneNumber: '',
247
+ },
248
+ } as Culture;
249
+
250
+ render(
251
+ <PhoneNumberInputA2 culture={cultureWithOptional} value="" onChange={jest.fn()} />
252
+ );
253
+
254
+ const input = screen.getByRole('textbox');
255
+ expect(input).toHaveAttribute('data-mask', '(999) 999-9999');
256
+ });
257
+ });
258
+
259
+ describe('props forwarding', () => {
260
+ it('forwards disabled, readOnly, onFocus, onBlur in non-SIP mode', () => {
261
+ const onFocus = jest.fn();
262
+ const onBlur = jest.fn();
263
+
264
+ render(
265
+ <PhoneNumberInputA2
266
+ culture={nanpCulture}
267
+ value=""
268
+ onChange={jest.fn()}
269
+ disabled
270
+ readOnly
271
+ onFocus={onFocus}
272
+ onBlur={onBlur}
273
+ />
274
+ );
275
+
276
+ const input = screen.getByRole('textbox');
277
+ expect(input).toBeDisabled();
278
+ expect(input).toHaveAttribute('readOnly');
279
+
280
+ fireEvent.focus(input);
281
+ expect(onFocus).toHaveBeenCalledTimes(1);
282
+
283
+ fireEvent.blur(input);
284
+ expect(onBlur).toHaveBeenCalledTimes(1);
285
+ });
286
+
287
+ it('forwards disabled, readOnly, onFocus, onBlur in SIP mode', () => {
288
+ const onFocus = jest.fn();
289
+ const onBlur = jest.fn();
290
+
291
+ render(
292
+ <PhoneNumberInputA2
293
+ sip
294
+ value=""
295
+ onChange={jest.fn()}
296
+ disabled
297
+ readOnly
298
+ onFocus={onFocus}
299
+ onBlur={onBlur}
300
+ />
301
+ );
302
+
303
+ const input = screen.getByRole('textbox');
304
+ expect(input).toBeDisabled();
305
+ expect(input).toHaveAttribute('readOnly');
306
+
307
+ fireEvent.focus(input);
308
+ expect(onFocus).toHaveBeenCalledTimes(1);
309
+
310
+ fireEvent.blur(input);
311
+ expect(onBlur).toHaveBeenCalledTimes(1);
312
+ });
313
+
314
+ it('does not call onChange when onChange is not provided', () => {
315
+ // Should not throw
316
+ render(<PhoneNumberInputA2 culture={nanpCulture} value="" />);
317
+
318
+ const input = screen.getByRole('textbox');
319
+ expect(() => {
320
+ fireEvent.change(input, { target: { value: '123' } });
321
+ }).not.toThrow();
322
+ });
323
+ });
324
+ });
@@ -26,11 +26,11 @@ export const PhoneNumberInputA2: FC<PhoneNumberInputA2Props> = props => {
26
26
 
27
27
  const [injectedCulture] = useOptionalDependencies(CULTURE_TOKEN);
28
28
  const culture = propCulture ?? injectedCulture;
29
- const { SimplePhoneMask = '?9999999999', SimplePhonePlaceholder = '' } =
29
+ const { SimplePhoneMask = '?9999999999', SampleSimplePhoneNumber = '' } =
30
30
  culture?.PhoneFormat ?? {};
31
31
 
32
32
  const mask = SimplePhoneMask.replace(/\?/g, '');
33
- const placeholder = propPlaceholder ?? SimplePhonePlaceholder;
33
+ const placeholder = propPlaceholder ?? SampleSimplePhoneNumber;
34
34
  const displayValue = String(value ?? '').replace(/\D/g, '');
35
35
 
36
36
  // Transform output value to digits-only (unmasked)
@@ -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
  }