@tcn/ui 0.9.0 → 0.11.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.
Files changed (298) hide show
  1. package/dist/bar.css +1 -0
  2. package/dist/body.css +1 -1
  3. package/dist/body.module-BbFZ7KNP.js +5 -0
  4. package/dist/body.module-BbFZ7KNP.js.map +1 -0
  5. package/dist/divider.css +1 -1
  6. package/dist/footer.css +1 -1
  7. package/dist/form/field/h_field/h_field.d.ts.map +1 -1
  8. package/dist/form/field/h_field/h_field.js +33 -35
  9. package/dist/form/field/h_field/h_field.js.map +1 -1
  10. package/dist/form/field/v_field/v_field.d.ts.map +1 -1
  11. package/dist/form/field/v_field/v_field.js +34 -36
  12. package/dist/form/field/v_field/v_field.js.map +1 -1
  13. package/dist/frame.css +1 -1
  14. package/dist/inputs/color_input/color_input.d.ts.map +1 -1
  15. package/dist/inputs/color_input/color_input.js +47 -46
  16. package/dist/inputs/color_input/color_input.js.map +1 -1
  17. package/dist/inputs/combo_box/combo_box.d.ts.map +1 -1
  18. package/dist/inputs/combo_box/combo_box.js +61 -58
  19. package/dist/inputs/combo_box/combo_box.js.map +1 -1
  20. package/dist/inputs/index.d.ts +1 -0
  21. package/dist/inputs/index.d.ts.map +1 -1
  22. package/dist/inputs/index.js +34 -31
  23. package/dist/inputs/index.js.map +1 -1
  24. package/dist/inputs/input/input.js +9 -9
  25. package/dist/inputs/input/input.js.map +1 -1
  26. package/dist/inputs/input_group/input_group.d.ts +5 -0
  27. package/dist/inputs/input_group/input_group.d.ts.map +1 -0
  28. package/dist/inputs/input_group/input_group.js +20 -0
  29. package/dist/inputs/input_group/input_group.js.map +1 -0
  30. package/dist/inputs/phone_number_input/countries_phone_information.d.ts +2 -2
  31. package/dist/inputs/phone_number_input/countries_phone_information.d.ts.map +1 -1
  32. package/dist/inputs/phone_number_input/countries_phone_information.js +5 -353
  33. package/dist/inputs/phone_number_input/countries_phone_information.js.map +1 -1
  34. package/dist/inputs/phone_number_input/phone_number_context.d.ts +24 -0
  35. package/dist/inputs/phone_number_input/phone_number_context.d.ts.map +1 -0
  36. package/dist/inputs/phone_number_input/phone_number_context.js +23 -0
  37. package/dist/inputs/phone_number_input/phone_number_context.js.map +1 -0
  38. package/dist/inputs/phone_number_input/phone_number_country_select_adapter.d.ts +19 -0
  39. package/dist/inputs/phone_number_input/phone_number_country_select_adapter.d.ts.map +1 -0
  40. package/dist/inputs/phone_number_input/phone_number_country_select_adapter.js +77 -0
  41. package/dist/inputs/phone_number_input/phone_number_country_select_adapter.js.map +1 -0
  42. package/dist/inputs/phone_number_input/phone_number_input.d.ts +16 -14
  43. package/dist/inputs/phone_number_input/phone_number_input.d.ts.map +1 -1
  44. package/dist/inputs/phone_number_input/phone_number_input.js +104 -274
  45. package/dist/inputs/phone_number_input/phone_number_input.js.map +1 -1
  46. package/dist/inputs/phone_number_input/phone_number_input_adapter.d.ts +6 -0
  47. package/dist/inputs/phone_number_input/phone_number_input_adapter.d.ts.map +1 -0
  48. package/dist/inputs/phone_number_input/phone_number_input_adapter.js +95 -0
  49. package/dist/inputs/phone_number_input/phone_number_input_adapter.js.map +1 -0
  50. package/dist/inputs/phone_number_input/sip_input.d.ts +12 -0
  51. package/dist/inputs/phone_number_input/sip_input.d.ts.map +1 -0
  52. package/dist/inputs/phone_number_input/sip_input.js +111 -0
  53. package/dist/inputs/phone_number_input/sip_input.js.map +1 -0
  54. package/dist/inputs/select/select.d.ts.map +1 -1
  55. package/dist/inputs/select/select.js +3 -2
  56. package/dist/inputs/select/select.js.map +1 -1
  57. package/dist/inputs/suggestions/suggestion_list.d.ts +4 -1
  58. package/dist/inputs/suggestions/suggestion_list.d.ts.map +1 -1
  59. package/dist/inputs/suggestions/suggestion_list.js +120 -111
  60. package/dist/inputs/suggestions/suggestion_list.js.map +1 -1
  61. package/dist/inputs/textarea/textarea.js +8 -8
  62. package/dist/inputs/textarea/textarea.js.map +1 -1
  63. package/dist/inputs/unit_input/unit_input.d.ts.map +1 -1
  64. package/dist/inputs/unit_input/unit_input.js +39 -39
  65. package/dist/inputs/unit_input/unit_input.js.map +1 -1
  66. package/dist/layouts/bar/bar.d.ts +5 -0
  67. package/dist/layouts/bar/bar.d.ts.map +1 -0
  68. package/dist/layouts/bar/bar.js +20 -0
  69. package/dist/layouts/bar/bar.js.map +1 -0
  70. package/dist/layouts/body/body.d.ts +2 -2
  71. package/dist/layouts/body/body.d.ts.map +1 -1
  72. package/dist/layouts/body/body.js +12 -12
  73. package/dist/layouts/body/body.js.map +1 -1
  74. package/dist/layouts/body/h_body.d.ts.map +1 -1
  75. package/dist/layouts/body/h_body.js +18 -12
  76. package/dist/layouts/body/h_body.js.map +1 -1
  77. package/dist/layouts/body/v_body.d.ts.map +1 -1
  78. package/dist/layouts/body/v_body.js +16 -10
  79. package/dist/layouts/body/v_body.js.map +1 -1
  80. package/dist/layouts/footer/footer.d.ts +2 -3
  81. package/dist/layouts/footer/footer.d.ts.map +1 -1
  82. package/dist/layouts/footer/footer.js +7 -7
  83. package/dist/layouts/footer/footer.js.map +1 -1
  84. package/dist/layouts/header/header.d.ts +2 -2
  85. package/dist/layouts/header/header.d.ts.map +1 -1
  86. package/dist/layouts/header/header.js +13 -21
  87. package/dist/layouts/header/header.js.map +1 -1
  88. package/dist/layouts/index.d.ts +0 -1
  89. package/dist/layouts/index.d.ts.map +1 -1
  90. package/dist/layouts/index.js +17 -19
  91. package/dist/layouts/index.js.map +1 -1
  92. package/dist/layouts/rail/rail.js +41 -41
  93. package/dist/layouts/rail/rail.js.map +1 -1
  94. package/dist/layouts/rail/side/side.d.ts.map +1 -1
  95. package/dist/layouts/rail/side/side.js +1 -1
  96. package/dist/layouts/rail/side/side.js.map +1 -1
  97. package/dist/layouts/rail/utility_strip/utility_strip.d.ts.map +1 -1
  98. package/dist/layouts/rail/utility_strip/utility_strip.js +21 -17
  99. package/dist/layouts/rail/utility_strip/utility_strip.js.map +1 -1
  100. package/dist/layouts/scaffold/scaffold.js +32 -32
  101. package/dist/layouts/scaffold/scaffold.js.map +1 -1
  102. package/dist/layouts/utility_bar/utility_bar.d.ts +2 -2
  103. package/dist/layouts/utility_bar/utility_bar.d.ts.map +1 -1
  104. package/dist/layouts/utility_bar/utility_bar.js +17 -19
  105. package/dist/layouts/utility_bar/utility_bar.js.map +1 -1
  106. package/dist/overlay/frame/frame.d.ts +8 -4
  107. package/dist/overlay/frame/frame.d.ts.map +1 -1
  108. package/dist/overlay/frame/frame.js +87 -23
  109. package/dist/overlay/frame/frame.js.map +1 -1
  110. package/dist/overlay/popper/base/dismissal_decorator.js.map +1 -1
  111. package/dist/overlay/popper/legacy/popper.d.ts.map +1 -1
  112. package/dist/overlay/popper/legacy/popper.js +52 -50
  113. package/dist/overlay/popper/legacy/popper.js.map +1 -1
  114. package/dist/panel.css +1 -0
  115. package/dist/phone_number_input.css +1 -1
  116. package/dist/rail.css +1 -1
  117. package/dist/scaffold.css +1 -1
  118. package/dist/side.css +1 -1
  119. package/dist/stacks/box/bottom_resize_handle.d.ts +2 -2
  120. package/dist/stacks/box/bottom_resize_handle.d.ts.map +1 -1
  121. package/dist/stacks/box/bottom_resize_handle.js +12 -10
  122. package/dist/stacks/box/bottom_resize_handle.js.map +1 -1
  123. package/dist/stacks/box/box.d.ts +4 -4
  124. package/dist/stacks/box/box.d.ts.map +1 -1
  125. package/dist/stacks/box/box.js +26 -26
  126. package/dist/stacks/box/box.js.map +1 -1
  127. package/dist/stacks/box/end_resize_handle.d.ts +2 -2
  128. package/dist/stacks/box/end_resize_handle.d.ts.map +1 -1
  129. package/dist/stacks/box/end_resize_handle.js +6 -5
  130. package/dist/stacks/box/end_resize_handle.js.map +1 -1
  131. package/dist/stacks/box/left_resize_handle.d.ts +2 -2
  132. package/dist/stacks/box/left_resize_handle.d.ts.map +1 -1
  133. package/dist/stacks/box/left_resize_handle.js +10 -9
  134. package/dist/stacks/box/left_resize_handle.js.map +1 -1
  135. package/dist/stacks/box/resize_handlers.d.ts +2 -2
  136. package/dist/stacks/box/resize_handlers.d.ts.map +1 -1
  137. package/dist/stacks/box/resize_handlers.js +32 -32
  138. package/dist/stacks/box/resize_handlers.js.map +1 -1
  139. package/dist/stacks/box/right_resize_handle.d.ts +2 -2
  140. package/dist/stacks/box/right_resize_handle.d.ts.map +1 -1
  141. package/dist/stacks/box/right_resize_handle.js +6 -5
  142. package/dist/stacks/box/right_resize_handle.js.map +1 -1
  143. package/dist/stacks/box/start_resize_handle.d.ts +2 -2
  144. package/dist/stacks/box/start_resize_handle.d.ts.map +1 -1
  145. package/dist/stacks/box/start_resize_handle.js +6 -6
  146. package/dist/stacks/box/start_resize_handle.js.map +1 -1
  147. package/dist/stacks/box/top_resize_handle.d.ts +2 -2
  148. package/dist/stacks/box/top_resize_handle.d.ts.map +1 -1
  149. package/dist/stacks/box/top_resize_handle.js +6 -5
  150. package/dist/stacks/box/top_resize_handle.js.map +1 -1
  151. package/dist/stacks/h_collapsible_box.d.ts.map +1 -1
  152. package/dist/stacks/h_collapsible_box.js +25 -25
  153. package/dist/stacks/h_collapsible_box.js.map +1 -1
  154. package/dist/stacks/v_collapsible_box.js +25 -25
  155. package/dist/stacks/v_collapsible_box.js.map +1 -1
  156. package/dist/suggestion_list.css +1 -1
  157. package/dist/surfaces/index.d.ts +1 -2
  158. package/dist/surfaces/index.d.ts.map +1 -1
  159. package/dist/surfaces/index.js +18 -20
  160. package/dist/surfaces/index.js.map +1 -1
  161. package/dist/surfaces/panel/panel.d.ts +5 -0
  162. package/dist/surfaces/panel/panel.d.ts.map +1 -0
  163. package/dist/surfaces/panel/panel.js +19 -0
  164. package/dist/surfaces/panel/panel.js.map +1 -0
  165. package/dist/surfaces/pop_confirm/pop_confirm.js +2 -3
  166. package/dist/surfaces/pop_confirm/pop_confirm.js.map +1 -1
  167. package/dist/surfaces/window/window.d.ts +1 -1
  168. package/dist/surfaces/window/window.d.ts.map +1 -1
  169. package/dist/surfaces/window/window.js +20 -10
  170. package/dist/surfaces/window/window.js.map +1 -1
  171. package/dist/themes/stylesheets/reset.css +1 -1
  172. package/dist/themes/stylesheets/reset.js +8 -1
  173. package/dist/themes/stylesheets/reset.js.map +1 -1
  174. package/dist/themes/theme.d.ts +2 -1
  175. package/dist/themes/theme.d.ts.map +1 -1
  176. package/dist/themes/theme.js +16 -9
  177. package/dist/themes/theme.js.map +1 -1
  178. package/dist/themes/themes/ergo/ergo_theme.css +1 -1
  179. package/dist/themes/themes/ergo/ergo_theme.js +210 -18
  180. package/dist/themes/themes/ergo/ergo_theme.js.map +1 -1
  181. package/dist/typography/body_text/body_text.d.ts.map +1 -1
  182. package/dist/typography/body_text/body_text.js +12 -10
  183. package/dist/typography/body_text/body_text.js.map +1 -1
  184. package/dist/utils/dnd/hooks/use_drag_container.d.ts.map +1 -1
  185. package/dist/utils/dnd/hooks/use_drag_container.js +22 -19
  186. package/dist/utils/dnd/hooks/use_drag_container.js.map +1 -1
  187. package/package.json +4 -2
  188. package/src/form/field/h_field/h_field.tsx +0 -4
  189. package/src/form/field/v_field/v_field.stories.tsx +8 -0
  190. package/src/form/field/v_field/v_field.tsx +1 -4
  191. package/src/form/field_set/field_set.stories.tsx +2 -1
  192. package/src/inputs/__docs__/inputs.mdx +81 -0
  193. package/src/inputs/__docs__/inputs.stories.tsx +268 -0
  194. package/src/inputs/color_input/color_input.tsx +17 -17
  195. package/src/inputs/combo_box/combo_box.tsx +17 -13
  196. package/src/inputs/index.ts +2 -0
  197. package/src/inputs/input/input.tsx +1 -1
  198. package/src/inputs/input_group/input_group.tsx +26 -0
  199. package/src/inputs/phone_number_input/countries_phone_information.ts +6 -353
  200. package/src/inputs/phone_number_input/phone_number_context.tsx +32 -0
  201. package/src/inputs/phone_number_input/phone_number_country_select_adapter.tsx +126 -0
  202. package/src/inputs/phone_number_input/phone_number_input.module.css +5 -63
  203. package/src/inputs/phone_number_input/phone_number_input.stories.tsx +180 -150
  204. package/src/inputs/phone_number_input/phone_number_input.tsx +133 -400
  205. package/src/inputs/phone_number_input/phone_number_input_adapter.tsx +123 -0
  206. package/src/inputs/phone_number_input/sip_input.tsx +147 -0
  207. package/src/inputs/select/select.tsx +13 -14
  208. package/src/inputs/suggestions/suggestion_list.module.css +1 -0
  209. package/src/inputs/suggestions/suggestion_list.stories.tsx +12 -8
  210. package/src/inputs/suggestions/suggestion_list.tsx +24 -3
  211. package/src/inputs/textarea/textarea.tsx +1 -1
  212. package/src/inputs/unit_input/unit_input.tsx +17 -17
  213. package/src/layouts/__stories__/composed.stories.tsx +0 -55
  214. package/src/layouts/__stories__/rail.stories.tsx +78 -0
  215. package/src/layouts/__stories__/scaffold.stories.tsx +90 -0
  216. package/src/layouts/__stories__/utils/content.tsx +27 -0
  217. package/src/layouts/__stories__/utils/layout_theme.css +88 -0
  218. package/src/layouts/__stories__/utils/layout_theme_provider.tsx +11 -0
  219. package/src/layouts/__stories__/utils.tsx +6 -6
  220. package/src/layouts/{utility_bar/utility_bar.module.css → bar/bar.module.css} +2 -1
  221. package/src/layouts/bar/bar.tsx +23 -0
  222. package/src/layouts/body/body.module.css +9 -4
  223. package/src/layouts/body/body.tsx +7 -6
  224. package/src/layouts/body/h_body.module.css +1 -2
  225. package/src/layouts/body/h_body.tsx +9 -4
  226. package/src/layouts/body/v_body.tsx +9 -4
  227. package/src/layouts/divider/divider.module.css +1 -1
  228. package/src/layouts/footer/footer.module.css +0 -3
  229. package/src/layouts/footer/footer.tsx +5 -6
  230. package/src/layouts/header/header.tsx +6 -15
  231. package/src/layouts/index.ts +0 -1
  232. package/src/layouts/rail/rail.module.css +9 -5
  233. package/src/layouts/rail/rail.tsx +1 -1
  234. package/src/layouts/rail/side/side.module.css +0 -1
  235. package/src/layouts/rail/side/side.tsx +1 -2
  236. package/src/layouts/rail/utility_strip/utility_strip.module.css +5 -0
  237. package/src/layouts/rail/utility_strip/utility_strip.tsx +4 -1
  238. package/src/layouts/scaffold/scaffold.module.css +10 -7
  239. package/src/layouts/scaffold/scaffold.tsx +1 -1
  240. package/src/layouts/utility_bar/utility_bar.tsx +6 -9
  241. package/src/overlay/frame/frame.module.css +2 -4
  242. package/src/overlay/frame/frame.stories.tsx +13 -10
  243. package/src/overlay/frame/frame.tsx +124 -16
  244. package/src/overlay/popper/base/dismissal_decorator.tsx +1 -1
  245. package/src/overlay/popper/legacy/popper.tsx +5 -1
  246. package/src/stacks/box/bottom_resize_handle.tsx +12 -5
  247. package/src/stacks/box/box.tsx +18 -14
  248. package/src/stacks/box/end_resize_handle.tsx +11 -6
  249. package/src/stacks/box/left_resize_handle.tsx +9 -3
  250. package/src/stacks/box/resize_handlers.ts +27 -13
  251. package/src/stacks/box/right_resize_handle.tsx +9 -3
  252. package/src/stacks/box/start_resize_handle.tsx +9 -4
  253. package/src/stacks/box/top_resize_handle.tsx +10 -4
  254. package/src/stacks/collapsible_box.stories.tsx +11 -11
  255. package/src/stacks/h_collapsible_box.tsx +5 -5
  256. package/src/stacks/v_collapsible_box.tsx +4 -4
  257. package/src/surfaces/index.ts +1 -2
  258. package/src/surfaces/panel/__stories__/panel.stories.tsx +12 -12
  259. package/src/surfaces/panel/__stories__/panel_stories.module.css +3 -3
  260. package/src/surfaces/panel/panel.module.css +1 -6
  261. package/src/surfaces/panel/panel.tsx +22 -0
  262. package/src/surfaces/window/window.tsx +14 -4
  263. package/src/themes/stories/controls_fieldset.tsx +1 -1
  264. package/src/themes/stylesheets/reset.css +8 -1
  265. package/src/themes/theme.tsx +6 -2
  266. package/src/themes/themes/ergo/__stories__/material.stories.tsx +15 -16
  267. package/src/themes/themes/ergo/ergo_theme.css +210 -18
  268. package/src/typography/body_text/body_text.tsx +2 -0
  269. package/src/utils/dnd/__stories__/draggable.stories.tsx +14 -8
  270. package/src/utils/dnd/hooks/use_drag_container.ts +13 -3
  271. package/dist/h_body.css +0 -1
  272. package/dist/h_panel.css +0 -1
  273. package/dist/header.css +0 -1
  274. package/dist/layouts/rail/main/main.d.ts +0 -6
  275. package/dist/layouts/rail/main/main.d.ts.map +0 -1
  276. package/dist/layouts/rail/main/main.js +0 -21
  277. package/dist/layouts/rail/main/main.js.map +0 -1
  278. package/dist/main.css +0 -1
  279. package/dist/surfaces/panel/h_panel.d.ts +0 -9
  280. package/dist/surfaces/panel/h_panel.d.ts.map +0 -1
  281. package/dist/surfaces/panel/h_panel.js +0 -60
  282. package/dist/surfaces/panel/h_panel.js.map +0 -1
  283. package/dist/surfaces/panel/v_panel.d.ts +0 -5
  284. package/dist/surfaces/panel/v_panel.d.ts.map +0 -1
  285. package/dist/surfaces/panel/v_panel.js +0 -19
  286. package/dist/surfaces/panel/v_panel.js.map +0 -1
  287. package/dist/utility_bar.css +0 -1
  288. package/dist/v_body.css +0 -1
  289. package/src/inputs/phone_number_input/__tests__/utils.test.ts +0 -52
  290. package/src/layouts/header/header.module.css +0 -8
  291. package/src/layouts/rail/__stories__/rail.stories.tsx +0 -64
  292. package/src/layouts/rail/__stories__/rail_stories.module.css +0 -25
  293. package/src/layouts/rail/main/main.module.css +0 -7
  294. package/src/layouts/rail/main/main.tsx +0 -26
  295. package/src/layouts/scaffold/__stories__/scaffold.stories.tsx +0 -53
  296. package/src/layouts/scaffold/__stories__/scaffold_stories.module.css +0 -31
  297. package/src/surfaces/panel/h_panel.tsx +0 -65
  298. package/src/surfaces/panel/v_panel.tsx +0 -20
@@ -1,428 +1,161 @@
1
+ import React, { useLayoutEffect } from 'react';
2
+ import PhoneInput from 'react-phone-number-input';
3
+ import 'react-phone-number-input/style.css';
4
+ import type { CountryCode } from 'libphonenumber-js';
1
5
  import { HStack, type HStackProps } from '../../stacks/h_stack.js';
2
- import { clsx } from 'clsx';
3
- import React, {
4
- useCallback,
5
- useLayoutEffect,
6
- useMemo,
7
- useRef,
8
- useState,
9
- Children,
10
- isValidElement,
11
- } from 'react';
12
- import { SlimButton } from '../../actions/index.js';
13
- import { Select } from '../select/select.js';
14
- import { MaskInput } from '../mask_input/mask_input.js';
15
- import {
16
- CountryPhoneInformation,
17
- countriesPhoneInformation,
18
- } from './countries_phone_information.js';
19
6
  import styles from './phone_number_input.module.css';
20
- import { NotebookIcon } from '@tcn/icons/notebook_icon.js';
21
- import { Option, OptionProps } from '../options/option.js';
22
- import { SuggestionList } from '../suggestions/suggestion_list.js';
23
- import { stripNonNumericAfterCountryCode } from './utils.js';
24
- import { useForkRef } from '../../utils/index.js';
25
-
26
- const OBFUSCATED_CHARACTER = '';
27
-
28
- function createObfuscatedMasks(masks: { mask: string; placeholder?: string }[]) {
29
- return masks.map(m => ({
30
- ...m,
31
- placeholder: m.mask.replace(/[9a*]/g, OBFUSCATED_CHARACTER),
32
- }));
33
- }
34
-
35
- const countryList = countriesPhoneInformation.map(i => ({
36
- name: i.name,
37
- selectedLabel: i.prefix,
38
- optionLabel: `${i.prefix} (${i.code}) ${i.unicodeFlag}`,
39
- value: i.code,
40
- keywords: i.keywords,
41
- }));
42
-
43
- countryList.sort((a, b) => parseInt(a.value) - parseInt(b.value));
44
-
45
- const countryPrefixMap: Map<string, CountryPhoneInformation[]> = new Map();
46
- const countryCodeMap: Map<string, CountryPhoneInformation> = new Map();
47
- countriesPhoneInformation.forEach(i => {
48
- countryCodeMap.set(i.code, i);
49
- if (countryPrefixMap.has(i.prefix)) {
50
- countryPrefixMap.get(i.prefix)?.push(i);
51
- } else {
52
- countryPrefixMap.set(i.prefix, [i]);
53
- }
54
- });
55
-
56
- function createCountryOptions(allowedCountryCodes?: string[]) {
57
- if (allowedCountryCodes != null) {
58
- const allowedMap: Record<string, boolean> = {};
59
- allowedCountryCodes.forEach(c => {
60
- allowedMap[c] = true;
61
- });
62
- return countryList
63
- .filter(c => Boolean(allowedMap[c.value]))
64
- .map(i => {
65
- return (
66
- <Option
67
- key={i.value}
68
- value={i.value}
69
- label={i.selectedLabel}
70
- keywords={i.keywords}
71
- >
72
- {i.optionLabel}
73
- </Option>
74
- );
75
- });
76
- } else {
77
- return countryList.map(i => {
78
- return (
79
- <Option
80
- key={i.value}
81
- value={i.value}
82
- label={i.selectedLabel}
83
- keywords={i.keywords}
84
- >
85
- {i.optionLabel}
86
- </Option>
87
- );
88
- });
89
- }
90
- }
91
-
92
- function getCountryCodeFromValue(
93
- value: string,
94
- previousCountry: string,
95
- defaultCountry: string
96
- ) {
97
- for (let x = 5; x > 1; x--) {
98
- const prefix = value.slice(0, x);
99
- const countriesInformation = countryPrefixMap.get(prefix);
100
-
101
- if (countriesInformation != null) {
102
- const countryInformation = countriesInformation.find(
103
- c => c.code === previousCountry
104
- );
105
-
106
- if (countryInformation != null) {
107
- return countryInformation;
108
- } else {
109
- return countriesInformation[0];
110
- }
111
- }
112
- }
113
-
114
- return (countryCodeMap.get(defaultCountry.toUpperCase()) ||
115
- countryCodeMap.get('US')) as CountryPhoneInformation;
116
- }
7
+ import { PhoneBookProvider, type PhoneContext } from './phone_number_context.js';
8
+ import { PhoneNumberCountrySelectAdapter } from './phone_number_country_select_adapter.js';
9
+ import { type OptionProps, Option } from '../options/option.js';
10
+ import { PhoneNumberInputAdapter } from './phone_number_input_adapter.js';
11
+ import { clsx } from 'clsx';
12
+ import { SipInput } from './sip_input.js';
13
+ import { defaultCountries } from './countries_phone_information.js';
117
14
 
118
15
  export interface PhoneNumberInputProps
119
16
  extends Omit<HStackProps, 'onChange' | 'children'> {
120
17
  value?: string;
121
- name?: string;
122
- autoComplete?: string;
18
+ allowSip?: boolean;
19
+ onChange: (value?: string) => void;
123
20
  defaultCountry?: string;
124
- /**
125
- * Callback fired when the phone number value changes.
126
- * @param value - The phone number value with country prefix
127
- * @param obfuscate - Whether the selected phone number is obfuscated (e.g., from a phone book entry marked as obfuscated)
128
- */
129
- onChange?: (value: string, obfuscate: boolean) => void;
130
- countrySelectRef?: React.Ref<HTMLButtonElement>;
131
- phoneNumberInputRef?: React.Ref<HTMLInputElement>;
132
- disabled?: boolean;
133
- disabledPhoneNumber?: boolean;
134
21
  allowedCountryCodes?: string[];
22
+ disabled?: boolean;
23
+ name?: string;
24
+ 'aria-label'?: string;
25
+ autoFocus?: boolean;
26
+ placeholder?: string;
27
+ className?: string;
28
+ width?: string;
29
+ onCountryChange?: (country: CountryCode) => void;
30
+ limitMaxLength?: boolean;
31
+ ariaSelectLabel?: string;
32
+ ariaPhoneBookButtonLabel?: string;
135
33
  children?: React.ReactElement<OptionProps> | React.ReactElement<OptionProps>[];
136
34
  }
137
35
 
138
- export const PhoneNumberInput = React.forwardRef(function PhoneNumberInput(
139
- {
140
- value = '',
141
- name,
142
- autoComplete,
143
- defaultCountry = 'US',
144
- onChange,
145
- countrySelectRef: countryRef,
146
- phoneNumberInputRef: numberRef,
147
- disabled = false,
148
- disabledPhoneNumber = false,
149
- allowedCountryCodes,
150
- children,
151
- ...props
152
- }: PhoneNumberInputProps,
153
- ref: React.Ref<HTMLElement>
154
- ) {
155
- const lastOutputValueRef = useRef(value);
156
- const [phoneBookElement, setPhoneBookElement] = useState<HTMLButtonElement | null>(
157
- null
36
+ export function PhoneNumberInput({
37
+ value = '',
38
+ allowSip = true,
39
+ onChange,
40
+ defaultCountry = 'US',
41
+ allowedCountryCodes: countries,
42
+ disabled,
43
+ name,
44
+ 'aria-label': ariaLabel,
45
+ autoFocus,
46
+ placeholder,
47
+ onCountryChange,
48
+ limitMaxLength,
49
+ ariaSelectLabel,
50
+ ariaPhoneBookButtonLabel,
51
+ children,
52
+ ...props
53
+ }: PhoneNumberInputProps) {
54
+ const [focusNumberInput, setFocusNumberInput] = React.useState<boolean>(false);
55
+ const isSip = value?.toLocaleLowerCase().startsWith('sip:') || false;
56
+ const [cachedNumber, setCachedNumber] = React.useState<string>(isSip ? '' : value);
57
+ const [sipAddress, setSipAddress] = React.useState<string>(
58
+ isSip ? value?.substring(4).trim() : ''
158
59
  );
159
- const isPhoneBookOpen = phoneBookElement != null;
160
- const [selectedCountry, setSelectedCountry] = useState(defaultCountry);
161
- const countryInformation = getCountryCodeFromValue(
162
- value,
163
- selectedCountry,
164
- defaultCountry
60
+ const [country, setCountry] = React.useState<CountryCode | undefined>(
61
+ defaultCountry as CountryCode
165
62
  );
166
- const [phoneNumber, setPhoneNumber] = useState(() => {
167
- const phoneNumber = value.split(countryInformation.prefix)[1];
168
63
 
169
- return phoneNumber == null ? '' : stripNonNumericAfterCountryCode(phoneNumber);
170
- });
64
+ countries = countries || defaultCountries;
65
+
66
+ useLayoutEffect(() => {
67
+ if (isSip) {
68
+ setSipAddress(value?.substring(4).trim() || '');
69
+ }
70
+ }, [isSip, value]);
71
+
72
+ useLayoutEffect(() => {
73
+ if (focusNumberInput) {
74
+ setFocusNumberInput(false);
75
+ }
76
+ }, [focusNumberInput]);
171
77
 
172
78
  // Extract valid Option components from children
173
- const phoneBookOptions = useMemo(() => {
174
- return Children.toArray(children).filter(
79
+ const phoneBookOptions = React.useMemo(() => {
80
+ return React.Children.toArray(children).filter(
175
81
  (child): child is React.ReactElement<OptionProps> =>
176
- isValidElement(child) && child.type === Option
82
+ React.isValidElement(child) && child.type === Option
177
83
  );
178
84
  }, [children]);
179
85
 
180
- const showPhoneBook = phoneBookOptions.length > 0;
181
- const [countryCode, setCountryCode] = useState(countryInformation.code);
182
- const [currentMasks, setCurrentMasks] = useState([
183
- ...countriesPhoneInformation[0].masks,
86
+ const phoneContext = React.useMemo<PhoneContext>(() => {
87
+ return {
88
+ value,
89
+ allowSip,
90
+ phoneBook: phoneBookOptions,
91
+ setValue: onChange,
92
+ ariaPhoneBookButtonLabel,
93
+ ariaSelectLabel,
94
+ disabled,
95
+ setCountry,
96
+ sipAddress,
97
+ setSipAddress,
98
+ cachedNumber,
99
+ setCachedNumber,
100
+ focusNumberInput,
101
+ setFocusNumberInput,
102
+ };
103
+ }, [
104
+ value,
105
+ allowSip,
106
+ phoneBookOptions,
107
+ onChange,
108
+ ariaPhoneBookButtonLabel,
109
+ ariaSelectLabel,
110
+ disabled,
111
+ sipAddress,
112
+ cachedNumber,
113
+ focusNumberInput,
184
114
  ]);
185
- const [obfuscateValue, setObfuscateValue] = useState(false);
186
- const [shouldFocusAfterClear, setShouldFocusAfterClear] = useState(false);
187
- const internalInputRef = useRef<HTMLInputElement>(null);
188
- const forkedInputRef = useForkRef(numberRef, internalInputRef);
189
-
190
- const countryOptions = useMemo(() => {
191
- return createCountryOptions(allowedCountryCodes);
192
- }, [allowedCountryCodes]);
193
-
194
- function changeCountry(countryCodeValue: string) {
195
- const countryInformation = countryCodeMap.get(countryCodeValue);
196
-
197
- if (countryInformation == null) {
198
- return;
199
- }
200
-
201
- setCountryCode(countryInformation.code);
202
- setCurrentMasks([...countryInformation.masks]);
203
- setSelectedCountry(countryInformation.code);
204
115
 
205
- if (phoneNumber == null) {
206
- return;
207
- }
208
-
209
- const value = `${countryInformation.prefix}${stripNonNumericAfterCountryCode(phoneNumber)}`;
210
- lastOutputValueRef.current = value;
211
- onChange && onChange(value, false);
212
- }
213
-
214
- useLayoutEffect(() => {
215
- const countryInformation = getCountryCodeFromValue(
216
- value,
217
- selectedCountry,
218
- defaultCountry
116
+ if (isSip) {
117
+ return (
118
+ <PhoneBookProvider value={phoneContext}>
119
+ <SipInput
120
+ onChange={onChange}
121
+ disabled={disabled}
122
+ name={name}
123
+ aria-label={ariaLabel}
124
+ autoFocus={autoFocus}
125
+ placeholder={placeholder}
126
+ countries={countries as CountryCode[]}
127
+ />
128
+ </PhoneBookProvider>
219
129
  );
220
- setCountryCode(countryInformation.code);
221
- setCurrentMasks([...countryInformation.masks]);
222
- setSelectedCountry(countryInformation.code);
223
- }, [value, selectedCountry, defaultCountry]);
224
-
225
- function transformValue(newPhoneNumber: string) {
226
- const countryPrefix = countryCodeMap.get(countryCode)?.prefix;
227
- const lineNumber = stripNonNumericAfterCountryCode(newPhoneNumber);
228
- const outputValue = countryPrefix + lineNumber;
229
-
230
- // Clear obfuscated state when user types manually
231
- if (obfuscateValue) {
232
- setObfuscateValue(false);
233
- }
234
-
235
- lastOutputValueRef.current = outputValue;
236
- phoneNumber !== newPhoneNumber && setPhoneNumber(newPhoneNumber);
237
- onChange && onChange(outputValue, false);
238
- }
239
-
240
- function togglePhoneBook(e: React.MouseEvent<HTMLButtonElement>) {
241
- if (isPhoneBookOpen) {
242
- setPhoneBookElement(null);
243
- } else {
244
- setPhoneBookElement(e.currentTarget);
245
- }
246
- }
247
-
248
- function closePhoneBook() {
249
- setPhoneBookElement(null);
250
- }
251
-
252
- function handlePhoneBookOptionSelect(
253
- value: string,
254
- _label: string | undefined,
255
- _isSuggestion: boolean,
256
- obfuscate: boolean
257
- ) {
258
- // Update the phone number with the selected value
259
- setObfuscateValue(obfuscate);
260
- updatePhoneNumber(value, obfuscate);
261
- closePhoneBook();
262
- }
263
-
264
- function preparePasteValue(value: string) {
265
- if (value.startsWith('+')) {
266
- const countryInformation = getCountryCodeFromValue(
267
- value,
268
- selectedCountry,
269
- defaultCountry
270
- );
271
- setCountryCode(countryInformation.code);
272
- setCurrentMasks([...countryInformation.masks]);
273
-
274
- const phoneNumber = value.split(countryInformation.prefix)[1];
275
- setPhoneNumber(phoneNumber);
276
-
277
- return phoneNumber;
278
- }
279
-
280
- return value;
281
- }
282
-
283
- function handleObfuscatedInputChange(newValue: string) {
284
- // When user types on a obfuscated input, clear the obfuscated state and start fresh
285
- // The newValue will be the digits the user typed (mask filters to valid input)
286
- setShouldFocusAfterClear(true);
287
- setObfuscateValue(false);
288
- setPhoneNumber(newValue);
289
-
290
- const countryPrefix = countryCodeMap.get(countryCode)?.prefix;
291
- const lineNumber = stripNonNumericAfterCountryCode(newValue);
292
- const outputValue = countryPrefix + lineNumber;
293
- lastOutputValueRef.current = outputValue;
294
- onChange && onChange(outputValue, false);
295
130
  }
296
131
 
297
- const updatePhoneNumber = useCallback(
298
- (value: string, obfuscate = false) => {
299
- const oldValue = lastOutputValueRef.current;
300
- const countryInformation = getCountryCodeFromValue(
301
- value,
302
- selectedCountry,
303
- defaultCountry
304
- );
305
- const phoneNumber = value.split(countryInformation.prefix)[1];
306
- setCountryCode(countryInformation.code);
307
- setCurrentMasks([...countryInformation.masks]);
308
- setSelectedCountry(countryInformation.code);
309
-
310
- if (oldValue !== value) {
311
- setPhoneNumber(phoneNumber);
312
- onChange && onChange(value, obfuscate);
313
- }
314
- },
315
- [defaultCountry, selectedCountry, onChange]
316
- );
317
-
318
- useLayoutEffect(() => {
319
- updatePhoneNumber(value);
320
- }, [value, updatePhoneNumber]);
321
-
322
- // Focus the input after transitioning from obfuscated to normal mode
323
- useLayoutEffect(() => {
324
- if (shouldFocusAfterClear && !obfuscateValue && internalInputRef.current) {
325
- internalInputRef.current.focus();
326
- setShouldFocusAfterClear(false);
327
- }
328
- }, [shouldFocusAfterClear, obfuscateValue]);
329
-
330
132
  return (
331
- <HStack
332
- ref={ref}
333
- className={clsx(styles['phone-number-input'], 'tcn-phone-number-input')}
334
- height="auto"
335
- {...props}
336
- >
337
- <Select
338
- className={clsx(
339
- styles['phone-number-input-select'],
340
- 'tcn-phone-number-input-select'
341
- )}
342
- ref={countryRef}
343
- width="auto"
344
- value={obfuscateValue ? '' : countryCode}
345
- onChange={changeCountry}
346
- disabled={disabled || obfuscateValue || disabledPhoneNumber}
347
- data-is-disabled={disabled || obfuscateValue || disabledPhoneNumber}
348
- data-is-obfuscated={obfuscateValue}
349
- placeholder={obfuscateValue ? '––' : undefined}
350
- >
351
- {countryOptions}
352
- </Select>
353
- <HStack
354
- width="flex"
355
- className={clsx(
356
- styles['phone-number-input-container'],
357
- 'tcn-phone-number-input-container'
358
- )}
359
- >
360
- {obfuscateValue ? (
361
- <MaskInput
362
- key="obfuscated"
363
- name={name}
364
- autoComplete={autoComplete}
365
- ref={forkedInputRef}
366
- value=""
367
- mask={createObfuscatedMasks(currentMasks)}
368
- onChange={handleObfuscatedInputChange}
369
- disabled={disabled || disabledPhoneNumber}
370
- data-is-disabled={disabled || disabledPhoneNumber}
371
- data-has-phone-book={showPhoneBook}
372
- data-is-obfuscated={true}
373
- className={clsx(
374
- styles['phone-number-input'],
375
- styles['phone-number-input-obfuscated'],
376
- 'tcn-phone-number-input'
377
- )}
378
- preparePasteValue={() => ''}
379
- prepareCopyValue={() => ''}
380
- prepareCutValue={() => ''}
381
- />
382
- ) : (
383
- <MaskInput
384
- key="normal"
385
- name={name}
386
- autoComplete={autoComplete}
387
- ref={forkedInputRef}
388
- value={phoneNumber}
389
- mask={currentMasks}
390
- onChange={transformValue}
391
- disabled={disabled || disabledPhoneNumber}
392
- data-is-disabled={disabled || disabledPhoneNumber}
393
- data-has-phone-book={showPhoneBook}
394
- data-is-obfuscated={false}
395
- className={clsx(styles['phone-number-input'], 'tcn-phone-number-input')}
396
- preparePasteValue={preparePasteValue}
397
- />
398
- )}
399
- </HStack>
400
- {showPhoneBook && (
401
- <>
402
- <SlimButton
403
- disabled={disabled}
404
- className={clsx(
405
- styles['phone-number-input-phone-book'],
406
- 'tcn-phone-number-input-phone-book'
407
- )}
408
- onClick={togglePhoneBook}
409
- size="md"
410
- >
411
- <NotebookIcon size="md" />
412
- </SlimButton>
413
-
414
- {isPhoneBookOpen && (
415
- <SuggestionList
416
- anchorElement={phoneBookElement}
417
- onOptionSelect={handlePhoneBookOptionSelect}
418
- onClose={closePhoneBook}
419
- noSuggestionMessage="No phone numbers found"
420
- >
421
- {phoneBookOptions}
422
- </SuggestionList>
133
+ <PhoneBookProvider value={phoneContext}>
134
+ <HStack {...props}>
135
+ <PhoneInput
136
+ value={value}
137
+ onChange={onChange}
138
+ defaultCountry={country}
139
+ country={country}
140
+ countries={countries as CountryCode[]}
141
+ countrySelectComponent={PhoneNumberCountrySelectAdapter}
142
+ inputComponent={PhoneNumberInputAdapter}
143
+ disabled={disabled}
144
+ name={name}
145
+ aria-label={ariaLabel}
146
+ autoFocus={autoFocus}
147
+ placeholder={placeholder}
148
+ onCountryChange={onCountryChange}
149
+ limitMaxLength={limitMaxLength}
150
+ className={clsx(
151
+ styles['phone-number-input'],
152
+ 'tcn-phone-number-input',
153
+ 'tcn-input-group'
423
154
  )}
424
- </>
425
- )}
426
- </HStack>
155
+ displayInitialValueAsLocalNumber
156
+ addInternationalOption={false}
157
+ />
158
+ </HStack>
159
+ </PhoneBookProvider>
427
160
  );
428
- });
161
+ }
@@ -0,0 +1,123 @@
1
+ import { NotebookIcon } from '@tcn/icons/notebook_icon.js';
2
+ import clsx from 'clsx';
3
+ import { forwardRef, useState, useCallback, useLayoutEffect, useRef } from 'react';
4
+ import { Button } from '../../actions/index.js';
5
+ import { SuggestionList } from '../suggestions/suggestion_list.js';
6
+ import { usePhoneContext } from './phone_number_context.js';
7
+ import { getCountryFromValue } from './phone_number_country_select_adapter.js';
8
+ import { Input } from '../input/input.js';
9
+ import { useForkRef } from '../../utils/hooks/use_fork_ref.js';
10
+
11
+ /**
12
+ * Bridges `@tcn/ui/inputs` Input (onChange: (value, event?) => void)
13
+ * with react-phone-number-input's expectation (onChange: (event) => void).
14
+ */
15
+ export const PhoneNumberInputAdapter = forwardRef<
16
+ HTMLInputElement,
17
+ React.InputHTMLAttributes<HTMLInputElement>
18
+ >(function InputAdapter({ onChange, value = '', className, ...rest }, ref) {
19
+ value = value.toString();
20
+ const [phoneBookElement, setPhoneBookElement] = useState<HTMLButtonElement | null>(
21
+ null
22
+ );
23
+ const isPhoneBookOpen = phoneBookElement != null;
24
+ const internalInputRef = useRef<HTMLInputElement | null>(null);
25
+ const forkedRef = useForkRef(ref, internalInputRef);
26
+
27
+ const {
28
+ phoneBook: phoneBookOptions,
29
+ setValue,
30
+ ariaPhoneBookButtonLabel,
31
+ disabled,
32
+ setFocusNumberInput,
33
+ focusNumberInput,
34
+ } = usePhoneContext();
35
+
36
+ const showPhoneBook = phoneBookOptions.length > 0;
37
+
38
+ function togglePhoneBook(e: React.MouseEvent<HTMLButtonElement>) {
39
+ if (isPhoneBookOpen) {
40
+ setPhoneBookElement(null);
41
+ } else {
42
+ setPhoneBookElement(e.currentTarget);
43
+ }
44
+ }
45
+
46
+ function closePhoneBook() {
47
+ setPhoneBookElement(null);
48
+ }
49
+
50
+ function handlePhoneBookOptionSelect(value: string) {
51
+ closePhoneBook();
52
+ setFocusNumberInput(true);
53
+ setValue(value);
54
+ }
55
+
56
+ const handleChange = useCallback(
57
+ (value: string, event?: React.ChangeEvent<HTMLInputElement>) => {
58
+ if (!onChange) return;
59
+ const e =
60
+ event ??
61
+ ({
62
+ target: { value },
63
+ currentTarget: { value },
64
+ } as React.ChangeEvent<HTMLInputElement>);
65
+ onChange(e);
66
+ },
67
+ [onChange]
68
+ );
69
+
70
+ if (value.startsWith('+')) {
71
+ const country = getCountryFromValue(value);
72
+ const prefixLength = country?.prefix?.length || 0;
73
+ value = value.substring(prefixLength);
74
+ }
75
+
76
+ useLayoutEffect(() => {
77
+ const input = internalInputRef?.current;
78
+
79
+ if (input == null || !focusNumberInput) {
80
+ return;
81
+ }
82
+
83
+ requestAnimationFrame(() => {
84
+ if (input.value.length > 0) {
85
+ input.select();
86
+ } else {
87
+ input.focus();
88
+ }
89
+ });
90
+ }, [focusNumberInput]);
91
+
92
+ return (
93
+ <>
94
+ <Input
95
+ ref={forkedRef}
96
+ value={value}
97
+ {...(rest as any)}
98
+ className={clsx(className, 'tcn-input-group-slot')}
99
+ onChange={handleChange}
100
+ />
101
+ {showPhoneBook && (
102
+ <Button
103
+ disabled={disabled}
104
+ className={clsx('tcn-input-group-slot', 'tcn-phone-number-phone-book')}
105
+ aria-label={ariaPhoneBookButtonLabel}
106
+ onClick={togglePhoneBook}
107
+ size="md"
108
+ >
109
+ <NotebookIcon size="md" />
110
+ </Button>
111
+ )}
112
+ <SuggestionList
113
+ open={isPhoneBookOpen}
114
+ anchorElement={phoneBookElement}
115
+ onOptionSelect={handlePhoneBookOptionSelect}
116
+ onClose={closePhoneBook}
117
+ noSuggestionMessage="No phone numbers found"
118
+ >
119
+ {phoneBookOptions}
120
+ </SuggestionList>
121
+ </>
122
+ );
123
+ });