emoji-picker-react 4.22.3 → 5.0.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 (405) hide show
  1. package/README.md +331 -49
  2. package/dist/DomUtils/categoryNameFromDom.d.ts +1 -1
  3. package/dist/DomUtils/classNames.d.ts +24 -23
  4. package/dist/DomUtils/detectEmojyPartiallyBelowFold.d.ts +2 -2
  5. package/dist/DomUtils/elementPositionInRow.d.ts +5 -5
  6. package/dist/DomUtils/eventBelongsToPicker.d.ts +2 -0
  7. package/dist/DomUtils/focusElement.d.ts +5 -5
  8. package/dist/DomUtils/getActiveElement.d.ts +2 -2
  9. package/dist/DomUtils/isJsdom.d.ts +1 -0
  10. package/dist/DomUtils/isRtl.d.ts +1 -0
  11. package/dist/DomUtils/keyboardNavigation.d.ts +7 -8
  12. package/dist/DomUtils/scrollTo.d.ts +4 -5
  13. package/dist/DomUtils/selectors.d.ts +31 -35
  14. package/dist/EmojiPickerReact.d.ts +5 -5
  15. package/dist/Stylesheet/DedupedStyle.d.ts +13 -0
  16. package/dist/Stylesheet/stylesheet.d.ts +40 -37
  17. package/dist/components/ErrorBoundary.d.ts +15 -15
  18. package/dist/components/Layout/Absolute.d.ts +8 -8
  19. package/dist/components/Layout/Flex.d.ts +13 -13
  20. package/dist/components/Layout/Relative.d.ts +8 -8
  21. package/dist/components/Layout/Space.d.ts +7 -7
  22. package/dist/components/Reactions/BtnPlus.d.ts +2 -2
  23. package/dist/components/Reactions/DEFAULT_REACTIONS.d.ts +1 -1
  24. package/dist/components/Reactions/Reactions.d.ts +3 -2
  25. package/dist/components/atoms/Button.d.ts +11 -6
  26. package/dist/components/body/EmojiCategory.d.ts +13 -12
  27. package/dist/components/body/EmojiList.d.ts +7 -4
  28. package/dist/components/body/EmojiVariationPicker.d.ts +2 -2
  29. package/dist/components/body/MeasureEmoji.d.ts +2 -2
  30. package/dist/components/body/listComponents.d.ts +47 -0
  31. package/dist/components/context/ElementRefContext.d.ts +18 -16
  32. package/dist/components/context/PickerConfigContext.d.ts +14 -9
  33. package/dist/components/context/PickerContext.d.ts +57 -25
  34. package/dist/components/context/PickerDataContext.d.ts +23 -17
  35. package/dist/components/emoji/BaseEmojiProps.d.ts +14 -13
  36. package/dist/components/emoji/ClickableEmojiButton.d.ts +23 -15
  37. package/dist/components/emoji/Emoji.d.ts +18 -15
  38. package/dist/components/emoji/EmojiImg.d.ts +11 -11
  39. package/dist/components/emoji/ExportedEmoji.d.ts +11 -11
  40. package/dist/components/emoji/NativeEmoji.d.ts +6 -6
  41. package/dist/components/emoji/ViewOnlyEmoji.d.ts +3 -3
  42. package/dist/components/emoji/emojiStyles.d.ts +6 -1
  43. package/dist/components/footer/Preview.d.ts +7 -7
  44. package/dist/components/header/Header.d.ts +2 -2
  45. package/dist/components/header/Search/BtnClearSearch.d.ts +2 -2
  46. package/dist/components/header/Search/IcnSearch.d.ts +2 -2
  47. package/dist/components/header/Search/Search.d.ts +11 -3
  48. package/dist/components/header/SkinTonePicker/BtnSkinToneVariation.d.ts +12 -12
  49. package/dist/components/header/SkinTonePicker/SkinTonePicker.d.ts +11 -11
  50. package/dist/components/icons/svgIcons.d.ts +4 -0
  51. package/dist/components/main/PickerMainBehaviors.d.ts +4 -0
  52. package/dist/components/main/defaultAppearance.d.ts +12 -0
  53. package/dist/components/main/labelHeight.d.ts +1 -0
  54. package/dist/components/navigation/CategoryButton.d.ts +12 -12
  55. package/dist/components/navigation/CategoryNavIcon.d.ts +4 -4
  56. package/dist/components/navigation/CategoryNavigation.d.ts +19 -2
  57. package/dist/config/categoryConfig.d.ts +34 -34
  58. package/dist/config/cdnUrls.d.ts +2 -2
  59. package/dist/config/compareConfig.d.ts +10 -2
  60. package/dist/config/config.d.ts +166 -64
  61. package/dist/config/customEmojiConfig.d.ts +14 -14
  62. package/dist/config/defaultPickerConfiguration.d.ts +10 -0
  63. package/dist/config/mutableConfig.d.ts +32 -10
  64. package/dist/config/useConfig.d.ts +48 -36
  65. package/dist/data/defaultEmojiData.d.ts +9 -0
  66. package/dist/data/defaultEmojiData.js +40 -0
  67. package/dist/data/emojis-bn.d.mts +39 -0
  68. package/dist/data/emojis-bn.d.ts +38 -2
  69. package/dist/data/emojis-bn.js +25 -2
  70. package/dist/data/emojis-bn.mjs +2 -0
  71. package/dist/data/emojis-da.d.mts +39 -0
  72. package/dist/data/emojis-da.d.ts +38 -2
  73. package/dist/data/emojis-da.js +25 -2
  74. package/dist/data/emojis-da.mjs +2 -0
  75. package/dist/data/emojis-de.d.mts +39 -0
  76. package/dist/data/emojis-de.d.ts +38 -2
  77. package/dist/data/emojis-de.js +25 -2
  78. package/dist/data/emojis-de.mjs +2 -0
  79. package/dist/data/emojis-en-gb.d.mts +39 -0
  80. package/dist/data/emojis-en-gb.d.ts +38 -2
  81. package/dist/data/emojis-en-gb.js +25 -2
  82. package/dist/data/emojis-en-gb.mjs +2 -0
  83. package/dist/data/emojis-en.d.mts +39 -0
  84. package/dist/data/emojis-en.d.ts +38 -2
  85. package/dist/data/emojis-en.js +25 -2
  86. package/dist/data/emojis-en.mjs +2 -0
  87. package/dist/data/emojis-es-mx.d.mts +39 -0
  88. package/dist/data/emojis-es-mx.d.ts +38 -2
  89. package/dist/data/emojis-es-mx.js +25 -2
  90. package/dist/data/emojis-es-mx.mjs +2 -0
  91. package/dist/data/emojis-es.d.mts +39 -0
  92. package/dist/data/emojis-es.d.ts +38 -2
  93. package/dist/data/emojis-es.js +25 -2
  94. package/dist/data/emojis-es.mjs +2 -0
  95. package/dist/data/emojis-et.d.mts +39 -0
  96. package/dist/data/emojis-et.d.ts +38 -2
  97. package/dist/data/emojis-et.js +25 -2
  98. package/dist/data/emojis-et.mjs +2 -0
  99. package/dist/data/emojis-fi.d.mts +39 -0
  100. package/dist/data/emojis-fi.d.ts +38 -2
  101. package/dist/data/emojis-fi.js +25 -2
  102. package/dist/data/emojis-fi.mjs +2 -0
  103. package/dist/data/emojis-fr.d.mts +39 -0
  104. package/dist/data/emojis-fr.d.ts +38 -2
  105. package/dist/data/emojis-fr.js +25 -2
  106. package/dist/data/emojis-fr.mjs +2 -0
  107. package/dist/data/emojis-hi.d.mts +39 -0
  108. package/dist/data/emojis-hi.d.ts +38 -2
  109. package/dist/data/emojis-hi.js +25 -2
  110. package/dist/data/emojis-hi.mjs +2 -0
  111. package/dist/data/emojis-hu.d.mts +39 -0
  112. package/dist/data/emojis-hu.d.ts +38 -2
  113. package/dist/data/emojis-hu.js +25 -2
  114. package/dist/data/emojis-hu.mjs +2 -0
  115. package/dist/data/emojis-it.d.mts +39 -0
  116. package/dist/data/emojis-it.d.ts +38 -2
  117. package/dist/data/emojis-it.js +25 -2
  118. package/dist/data/emojis-it.mjs +2 -0
  119. package/dist/data/emojis-ja.d.mts +39 -0
  120. package/dist/data/emojis-ja.d.ts +38 -2
  121. package/dist/data/emojis-ja.js +25 -2
  122. package/dist/data/emojis-ja.mjs +2 -0
  123. package/dist/data/emojis-ko.d.mts +39 -0
  124. package/dist/data/emojis-ko.d.ts +38 -2
  125. package/dist/data/emojis-ko.js +25 -2
  126. package/dist/data/emojis-ko.mjs +2 -0
  127. package/dist/data/emojis-lt.d.mts +39 -0
  128. package/dist/data/emojis-lt.d.ts +38 -2
  129. package/dist/data/emojis-lt.js +25 -2
  130. package/dist/data/emojis-lt.mjs +2 -0
  131. package/dist/data/emojis-ms.d.mts +39 -0
  132. package/dist/data/emojis-ms.d.ts +38 -2
  133. package/dist/data/emojis-ms.js +25 -2
  134. package/dist/data/emojis-ms.mjs +2 -0
  135. package/dist/data/emojis-nb.d.mts +39 -0
  136. package/dist/data/emojis-nb.d.ts +38 -2
  137. package/dist/data/emojis-nb.js +25 -2
  138. package/dist/data/emojis-nb.mjs +2 -0
  139. package/dist/data/emojis-nl.d.mts +39 -0
  140. package/dist/data/emojis-nl.d.ts +38 -2
  141. package/dist/data/emojis-nl.js +25 -2
  142. package/dist/data/emojis-nl.mjs +2 -0
  143. package/dist/data/emojis-pl.d.mts +39 -0
  144. package/dist/data/emojis-pl.d.ts +38 -2
  145. package/dist/data/emojis-pl.js +25 -2
  146. package/dist/data/emojis-pl.mjs +2 -0
  147. package/dist/data/emojis-pt.d.mts +39 -0
  148. package/dist/data/emojis-pt.d.ts +38 -2
  149. package/dist/data/emojis-pt.js +25 -2
  150. package/dist/data/emojis-pt.mjs +2 -0
  151. package/dist/data/emojis-ru.d.mts +39 -0
  152. package/dist/data/emojis-ru.d.ts +38 -2
  153. package/dist/data/emojis-ru.js +25 -2
  154. package/dist/data/emojis-ru.mjs +2 -0
  155. package/dist/data/emojis-sv.d.mts +39 -0
  156. package/dist/data/emojis-sv.d.ts +38 -2
  157. package/dist/data/emojis-sv.js +25 -2
  158. package/dist/data/emojis-sv.mjs +2 -0
  159. package/dist/data/emojis-th.d.mts +39 -0
  160. package/dist/data/emojis-th.d.ts +38 -2
  161. package/dist/data/emojis-th.js +25 -2
  162. package/dist/data/emojis-th.mjs +2 -0
  163. package/dist/data/emojis-uk.d.mts +39 -0
  164. package/dist/data/emojis-uk.d.ts +38 -2
  165. package/dist/data/emojis-uk.js +25 -2
  166. package/dist/data/emojis-uk.mjs +2 -0
  167. package/dist/data/emojis-vi.d.mts +39 -0
  168. package/dist/data/emojis-vi.d.ts +38 -2
  169. package/dist/data/emojis-vi.js +25 -2
  170. package/dist/data/emojis-vi.mjs +2 -0
  171. package/dist/data/emojis-zh-hant.d.mts +39 -0
  172. package/dist/data/emojis-zh-hant.d.ts +38 -2
  173. package/dist/data/emojis-zh-hant.js +25 -2
  174. package/dist/data/emojis-zh-hant.mjs +2 -0
  175. package/dist/data/emojis-zh.d.mts +39 -0
  176. package/dist/data/emojis-zh.d.ts +38 -2
  177. package/dist/data/emojis-zh.js +25 -2
  178. package/dist/data/emojis-zh.mjs +2 -0
  179. package/dist/data/index.js +1 -0
  180. package/dist/data/registerDefaultEmojiData.d.ts +1 -0
  181. package/dist/data/registerDefaultEmojiData.js +3 -0
  182. package/dist/data-core/pickerData.d.ts +12 -0
  183. package/dist/data-core/prepare.d.ts +23 -0
  184. package/dist/data-core/search.d.ts +13 -0
  185. package/dist/data-core/types.d.ts +23 -0
  186. package/dist/data.d.mts +72 -0
  187. package/dist/data.d.ts +72 -0
  188. package/dist/dataUtils/DataTypes.d.ts +23 -19
  189. package/dist/dataUtils/emojiSelectors.d.ts +15 -16
  190. package/dist/dataUtils/emojiUtils.d.ts +1 -12
  191. package/dist/dataUtils/nativeEmojiSequenceSupport.d.ts +10 -0
  192. package/dist/dataUtils/nativeEmojiSupport.d.ts +40 -0
  193. package/dist/dataUtils/parseNativeEmoji.d.ts +1 -1
  194. package/dist/dataUtils/suggested.d.ts +11 -11
  195. package/dist/dataUtils/suggestedEmojis.d.ts +7 -0
  196. package/dist/esm/chunks/chunk-46BUDQLE.mjs +228 -0
  197. package/dist/esm/chunks/chunk-7ZGTUZFB.mjs +16 -0
  198. package/dist/esm/chunks/chunk-FQIJ4HBW.mjs +25 -0
  199. package/dist/esm/chunks/chunk-KQPOIRSC.mjs +9305 -0
  200. package/dist/esm/chunks/chunk-LO4TPPLH.mjs +43 -0
  201. package/dist/esm/chunks/emojis-6L3GRBAP.mjs +9 -0
  202. package/dist/esm/data.mjs +18 -0
  203. package/dist/esm/index.mjs +389 -0
  204. package/dist/esm/primitives/index.mjs +67 -0
  205. package/dist/hooks/preloadEmoji.d.ts +9 -9
  206. package/dist/hooks/useActiveCategoryScrollDetection.d.ts +11 -11
  207. package/dist/hooks/useCategoryHeight.d.ts +6 -5
  208. package/dist/hooks/useCloseAllOpenToggles.d.ts +2 -2
  209. package/dist/hooks/useDataIdentityStabilityWarning.d.ts +13 -0
  210. package/dist/hooks/useDebouncedState.d.ts +1 -1
  211. package/dist/hooks/useDisallowMouseMove.d.ts +4 -4
  212. package/dist/hooks/useDisallowedEmojis.d.ts +2 -3
  213. package/dist/hooks/useEmojiPreviewEvents.d.ts +3 -3
  214. package/dist/hooks/useEmojiVirtualization.d.ts +21 -17
  215. package/dist/hooks/useFilter.d.ts +10 -16
  216. package/dist/hooks/useFocus.d.ts +3 -3
  217. package/dist/hooks/useInitialLoad.d.ts +2 -2
  218. package/dist/hooks/useIsEmojiHidden.d.ts +8 -8
  219. package/dist/hooks/useIsMounted.d.ts +1 -1
  220. package/dist/hooks/useIsSearchMode.d.ts +1 -1
  221. package/dist/hooks/useIsomorphicLayoutEffect.d.ts +2 -2
  222. package/dist/hooks/useKeyboardNavigation.d.ts +3 -1
  223. package/dist/hooks/useMouseDownHandlers.d.ts +8 -4
  224. package/dist/hooks/useOnFocus.d.ts +1 -1
  225. package/dist/hooks/useOnScroll.d.ts +2 -2
  226. package/dist/hooks/useReactionsFocus.d.ts +1 -0
  227. package/dist/hooks/useRegisterRegion.d.ts +24 -0
  228. package/dist/hooks/useResolvedEmojiData.d.ts +37 -0
  229. package/dist/hooks/useScrollCategoryIntoView.d.ts +1 -1
  230. package/dist/hooks/useSearchController.d.ts +17 -0
  231. package/dist/hooks/useSearchResults.d.ts +8 -0
  232. package/dist/hooks/useSetVariationPicker.d.ts +2 -2
  233. package/dist/hooks/useShouldHideCustomEmojis.d.ts +1 -1
  234. package/dist/hooks/useShouldShowSkinTonePicker.d.ts +4 -4
  235. package/dist/index.d.mts +376 -0
  236. package/dist/index.d.ts +376 -9
  237. package/dist/index.js +2 -8
  238. package/dist/primitives/CategoryNav.d.ts +4 -0
  239. package/dist/primitives/Empty.d.ts +6 -0
  240. package/dist/primitives/List.d.ts +4 -0
  241. package/dist/primitives/LoadError.d.ts +8 -0
  242. package/dist/primitives/Loading.d.ts +4 -0
  243. package/dist/primitives/Panel.d.ts +3 -0
  244. package/dist/primitives/Preview.d.ts +3 -0
  245. package/dist/primitives/Reactions.d.ts +2 -0
  246. package/dist/primitives/Root.d.ts +26 -0
  247. package/dist/primitives/Search.d.ts +5 -0
  248. package/dist/primitives/SearchInput.d.ts +3 -0
  249. package/dist/primitives/SkinTone.d.ts +4 -0
  250. package/dist/primitives/Viewport.d.ts +19 -0
  251. package/dist/primitives/appearance.d.ts +3 -0
  252. package/dist/primitives/components.d.ts +27 -0
  253. package/dist/primitives/hooks.d.ts +48 -0
  254. package/dist/primitives/index.d.mts +561 -0
  255. package/dist/primitives/index.d.ts +561 -0
  256. package/dist/primitives/index.js +2 -0
  257. package/dist/primitives/nativeProps.d.ts +15 -0
  258. package/dist/primitives/scope.d.ts +13 -0
  259. package/dist/primitives/structuralStyles.d.ts +16 -0
  260. package/dist/primitives/tokens.d.ts +18 -0
  261. package/dist/primitives/types.d.ts +85 -0
  262. package/dist/state/activeEmojiStore.d.ts +7 -0
  263. package/dist/state/navigationRegistry.d.ts +61 -0
  264. package/dist/state/regionTraversal.d.ts +20 -0
  265. package/dist/typeRefinements/typeRefinements.d.ts +6 -6
  266. package/dist/types/exposedTypes.d.ts +16 -6
  267. package/dist/types/exposedTypes.js +5 -0
  268. package/dist/virtualization/virtualizationHelpers.d.ts +20 -18
  269. package/llms-full.txt +3680 -0
  270. package/llms.txt +66 -480
  271. package/package.json +180 -66
  272. package/src/DomUtils/categoryNameFromDom.ts +1 -1
  273. package/src/DomUtils/classNames.ts +1 -0
  274. package/src/DomUtils/elementPositionInRow.ts +26 -4
  275. package/src/DomUtils/eventBelongsToPicker.ts +17 -0
  276. package/src/DomUtils/getActiveElement.ts +7 -1
  277. package/src/DomUtils/isJsdom.ts +12 -0
  278. package/src/DomUtils/isRtl.ts +5 -0
  279. package/src/DomUtils/keyboardNavigation.ts +105 -149
  280. package/src/DomUtils/scrollTo.ts +18 -16
  281. package/src/DomUtils/selectors.ts +20 -111
  282. package/src/EmojiPickerReact.tsx +155 -48
  283. package/src/Stylesheet/DedupedStyle.tsx +80 -0
  284. package/src/Stylesheet/stylesheet.tsx +128 -59
  285. package/src/components/Layout/Flex.tsx +12 -11
  286. package/src/components/Reactions/BtnPlus.tsx +61 -51
  287. package/src/components/Reactions/Reactions.tsx +113 -55
  288. package/src/components/atoms/Button.tsx +18 -15
  289. package/src/components/body/EmojiCategory.tsx +77 -40
  290. package/src/components/body/EmojiList.tsx +55 -23
  291. package/src/components/body/EmojiVariationPicker.tsx +113 -66
  292. package/src/components/body/MeasureEmoji.tsx +9 -2
  293. package/src/components/body/listComponents.tsx +79 -0
  294. package/src/components/context/ElementRefContext.tsx +46 -35
  295. package/src/components/context/PickerConfigContext.tsx +55 -40
  296. package/src/components/context/PickerContext.tsx +567 -92
  297. package/src/components/context/PickerDataContext.tsx +110 -99
  298. package/src/components/emoji/BaseEmojiProps.ts +5 -4
  299. package/src/components/emoji/ClickableEmojiButton.tsx +148 -63
  300. package/src/components/emoji/Emoji.tsx +24 -2
  301. package/src/components/emoji/EmojiImg.tsx +13 -12
  302. package/src/components/emoji/ExportedEmoji.tsx +5 -4
  303. package/src/components/emoji/NativeEmoji.tsx +19 -16
  304. package/src/components/emoji/ViewOnlyEmoji.tsx +55 -30
  305. package/src/components/emoji/emojiStyles.ts +12 -11
  306. package/src/components/footer/Preview.tsx +86 -91
  307. package/src/components/header/Header.tsx +21 -6
  308. package/src/components/header/Search/BtnClearSearch.tsx +69 -55
  309. package/src/components/header/Search/IcnSearch.tsx +21 -21
  310. package/src/components/header/Search/Search.tsx +77 -137
  311. package/src/components/header/SkinTonePicker/BtnSkinToneVariation.tsx +99 -62
  312. package/src/components/header/SkinTonePicker/SkinTonePicker.tsx +84 -46
  313. package/src/components/icons/svgIcons.ts +17 -0
  314. package/src/components/main/PickerMainBehaviors.tsx +163 -0
  315. package/src/components/main/defaultAppearance.tsx +188 -0
  316. package/src/components/main/labelHeight.ts +5 -0
  317. package/src/components/navigation/CategoryButton.tsx +83 -57
  318. package/src/components/navigation/CategoryNavIcon.tsx +73 -194
  319. package/src/components/navigation/CategoryNavigation.tsx +118 -46
  320. package/src/config/categoryConfig.ts +7 -3
  321. package/src/config/cdnUrls.ts +11 -21
  322. package/src/config/compareConfig.ts +128 -30
  323. package/src/config/config.ts +228 -24
  324. package/src/config/defaultPickerConfiguration.ts +7 -0
  325. package/src/config/mutableConfig.ts +44 -20
  326. package/src/config/useConfig.ts +130 -37
  327. package/src/data/defaultEmojiData.ts +49 -0
  328. package/src/data/registerDefaultEmojiData.ts +8 -0
  329. package/src/data-core/pickerData.ts +173 -0
  330. package/src/data-core/prepare.ts +178 -0
  331. package/src/data-core/search.ts +122 -0
  332. package/src/data-core/types.ts +36 -0
  333. package/src/data.ts +10 -0
  334. package/src/dataUtils/DataTypes.ts +4 -0
  335. package/src/dataUtils/emojiSelectors.ts +58 -63
  336. package/src/dataUtils/emojiUtils.ts +17 -74
  337. package/src/dataUtils/nativeEmojiSequenceSupport.ts +116 -0
  338. package/src/dataUtils/nativeEmojiSupport.ts +160 -0
  339. package/src/dataUtils/suggested.ts +8 -4
  340. package/src/dataUtils/suggestedEmojis.ts +92 -0
  341. package/src/env.d.ts +4 -0
  342. package/src/hooks/preloadEmoji.ts +3 -3
  343. package/src/hooks/useActiveCategoryScrollDetection.ts +10 -1
  344. package/src/hooks/useCategoryHeight.ts +82 -8
  345. package/src/hooks/useDataIdentityStabilityWarning.ts +86 -0
  346. package/src/hooks/useDebouncedState.ts +13 -1
  347. package/src/hooks/useDisallowedEmojis.ts +26 -42
  348. package/src/hooks/useEmojiPreviewEvents.ts +43 -10
  349. package/src/hooks/useEmojiVirtualization.tsx +38 -4
  350. package/src/hooks/useFilter.ts +85 -127
  351. package/src/hooks/useFocus.ts +14 -24
  352. package/src/hooks/useIsEmojiHidden.ts +10 -6
  353. package/src/hooks/useKeyboardNavigation.ts +322 -429
  354. package/src/hooks/useMouseDownHandlers.ts +91 -16
  355. package/src/hooks/useOnFocus.ts +12 -3
  356. package/src/hooks/useOnScroll.ts +18 -2
  357. package/src/hooks/useReactionsFocus.ts +118 -0
  358. package/src/hooks/useRegisterRegion.ts +92 -0
  359. package/src/hooks/useResolvedEmojiData.ts +200 -0
  360. package/src/hooks/useScrollCategoryIntoView.ts +74 -12
  361. package/src/hooks/useSearchController.ts +369 -0
  362. package/src/hooks/useSearchResults.ts +51 -0
  363. package/src/hooks/useSetVariationPicker.ts +4 -1
  364. package/src/index.tsx +67 -5
  365. package/src/primitives/CategoryNav.tsx +40 -0
  366. package/src/primitives/Empty.tsx +68 -0
  367. package/src/primitives/List.tsx +61 -0
  368. package/src/primitives/LoadError.tsx +38 -0
  369. package/src/primitives/Loading.tsx +60 -0
  370. package/src/primitives/Panel.tsx +47 -0
  371. package/src/primitives/Preview.tsx +38 -0
  372. package/src/primitives/Reactions.tsx +2 -0
  373. package/src/primitives/Root.tsx +398 -0
  374. package/src/primitives/Search.tsx +41 -0
  375. package/src/primitives/SearchInput.tsx +110 -0
  376. package/src/primitives/SkinTone.tsx +76 -0
  377. package/src/primitives/Viewport.tsx +204 -0
  378. package/src/primitives/appearance.tsx +7 -0
  379. package/src/primitives/components.tsx +30 -0
  380. package/src/primitives/hooks.ts +165 -0
  381. package/src/primitives/index.ts +94 -0
  382. package/src/primitives/nativeProps.ts +90 -0
  383. package/src/primitives/scope.tsx +109 -0
  384. package/src/primitives/structuralStyles.tsx +133 -0
  385. package/src/primitives/tokens.ts +149 -0
  386. package/src/primitives/types.ts +192 -0
  387. package/src/state/activeEmojiStore.ts +39 -0
  388. package/src/state/navigationRegistry.ts +209 -0
  389. package/src/state/regionTraversal.ts +116 -0
  390. package/src/svg.d.ts +2 -2
  391. package/src/types/exposedTypes.ts +28 -1
  392. package/src/virtualization/virtualizationHelpers.ts +20 -6
  393. package/dist/components/body/Body.d.ts +0 -2
  394. package/dist/components/main/PickerMain.d.ts +0 -7
  395. package/dist/dataUtils/alphaNumericEmojiIndex.d.ts +0 -5
  396. package/dist/emoji-picker-react.cjs.development.js +0 -31413
  397. package/dist/emoji-picker-react.cjs.development.js.map +0 -1
  398. package/dist/emoji-picker-react.cjs.production.min.js +0 -2
  399. package/dist/emoji-picker-react.cjs.production.min.js.map +0 -1
  400. package/dist/emoji-picker-react.esm.js +0 -31411
  401. package/dist/emoji-picker-react.esm.js.map +0 -1
  402. package/dist/hooks/useHideEmojisByUniocode.d.ts +0 -1
  403. package/src/components/body/Body.tsx +0 -43
  404. package/src/components/main/PickerMain.tsx +0 -249
  405. package/src/dataUtils/alphaNumericEmojiIndex.ts +0 -21
@@ -0,0 +1,200 @@
1
+ import * as React from 'react';
2
+
3
+ import {
4
+ EMPTY_EMOJI_DATA,
5
+ getRegisteredDefaultEmojiData,
6
+ loadDefaultEmojiData,
7
+ } from '../data/defaultEmojiData';
8
+ import type { EmojiData } from '../types/exposedTypes';
9
+
10
+ /**
11
+ * Asynchronous dataset source, e.g.
12
+ * `() => import('emoji-picker-react/data/emojis-fr')`.
13
+ * Hoist it (module scope or useCallback): a new function identity loads
14
+ * again.
15
+ */
16
+ export type EmojiDataLoaderOptions = { signal?: AbortSignal };
17
+ export type EmojiDataLoader = (
18
+ options: EmojiDataLoaderOptions,
19
+ ) => Promise<EmojiData | { default: EmojiData }>;
20
+
21
+ export type EmojiDataInput = EmojiData | EmojiDataLoader;
22
+
23
+ export type EmojiDataState = {
24
+ loading: boolean;
25
+ error: Error | null;
26
+ retry: () => void;
27
+ };
28
+ type Resolved = EmojiDataState & { data: EmojiData | undefined };
29
+
30
+ function isRecord(value: unknown): value is Record<string, unknown> {
31
+ return !!value && typeof value === 'object' && !Array.isArray(value);
32
+ }
33
+
34
+ function isStringArray(value: unknown): value is string[] {
35
+ return (
36
+ Array.isArray(value) && value.every((item) => typeof item === 'string')
37
+ );
38
+ }
39
+
40
+ function isUnified(value: string): boolean {
41
+ return value
42
+ .split('-')
43
+ .every(
44
+ (hex) => /^[\da-f]{1,6}$/i.test(hex) && parseInt(hex, 16) <= 0x10ffff,
45
+ );
46
+ }
47
+
48
+ function hasValidEncoding(value: Record<string, unknown>): boolean {
49
+ if (value.imgUrl !== undefined) return typeof value.imgUrl === 'string';
50
+ return (
51
+ typeof value.u === 'string' &&
52
+ isUnified(value.u) &&
53
+ (value.v === undefined ||
54
+ (isStringArray(value.v) && value.v.every(isUnified)))
55
+ );
56
+ }
57
+
58
+ function isEmoji(value: unknown): boolean {
59
+ return (
60
+ isRecord(value) &&
61
+ typeof value.u === 'string' &&
62
+ !!value.u &&
63
+ isStringArray(value.n) &&
64
+ value.n.length > 0 &&
65
+ typeof value.a === 'string' &&
66
+ hasValidEncoding(value)
67
+ );
68
+ }
69
+
70
+ function isEmojiGroup(value: unknown): boolean {
71
+ return Array.isArray(value) && value.every(isEmoji);
72
+ }
73
+
74
+ function isCategory(value: unknown): boolean {
75
+ return (
76
+ isRecord(value) &&
77
+ typeof value.category === 'string' &&
78
+ typeof value.name === 'string'
79
+ );
80
+ }
81
+
82
+ function normalize(result: EmojiData | { default: EmojiData }): EmojiData {
83
+ const data =
84
+ isRecord(result) && 'default' in result ? result.default : result;
85
+ if (
86
+ !isRecord(data) ||
87
+ !isRecord(data.categories) ||
88
+ !isRecord(data.emojis) ||
89
+ !Object.values(data.emojis).every(isEmojiGroup) ||
90
+ !Object.values(data.categories).every(isCategory)
91
+ ) {
92
+ throw new Error(
93
+ '[emoji-picker-react] emojiData loader must resolve a dataset or { default: dataset }.',
94
+ );
95
+ }
96
+ return data as EmojiData;
97
+ }
98
+
99
+ /**
100
+ * Resolve Root's `emojiData` input to a synchronous dataset:
101
+ * - an object is used as-is (synchronous, SSR-safe);
102
+ * - a loader is awaited (Root renders empty, `Loading` shows meanwhile);
103
+ * - absent: the registered default dataset when an entry registered it
104
+ * (the default picker always does), otherwise the bundled English
105
+ * dataset is loaded on demand (a separate chunk in ESM builds).
106
+ * `data: undefined` means "the registered default".
107
+ */
108
+ export function useResolvedEmojiData(
109
+ input?: EmojiDataInput,
110
+ enabled = true,
111
+ ): Resolved {
112
+ const needsLoad =
113
+ typeof input === 'function' ||
114
+ (input === undefined && getRegisteredDefaultEmojiData() === null);
115
+
116
+ const [loaded, setLoaded] = React.useState<{
117
+ source: EmojiDataInput | undefined;
118
+ data: EmojiData;
119
+ error: Error | null;
120
+ attempt: number;
121
+ } | null>(null);
122
+ const [attempt, setAttempt] = React.useState(0);
123
+ const retry = React.useCallback(() => setAttempt((value) => value + 1), []);
124
+
125
+ React.useEffect(() => {
126
+ if (!needsLoad || !enabled) {
127
+ return;
128
+ }
129
+ let cancelled = false;
130
+ const controller =
131
+ typeof AbortController === 'undefined'
132
+ ? undefined
133
+ : new AbortController();
134
+ // Invoke immediately, preserving the loader contract, and convert
135
+ // synchronous exceptions into the same recoverable failure state.
136
+ let load: Promise<EmojiData>;
137
+ try {
138
+ load =
139
+ typeof input === 'function'
140
+ ? Promise.resolve(input({ signal: controller?.signal })).then(
141
+ normalize,
142
+ )
143
+ : loadDefaultEmojiData();
144
+ } catch (error) {
145
+ load = Promise.reject(error);
146
+ }
147
+ load.then(
148
+ (data) => {
149
+ if (!cancelled) {
150
+ setLoaded({ source: input, data, error: null, attempt });
151
+ }
152
+ },
153
+ (error) => {
154
+ if (!cancelled) {
155
+ setLoaded({
156
+ source: input,
157
+ data: EMPTY_EMOJI_DATA,
158
+ attempt,
159
+ error: error instanceof Error ? error : new Error(String(error)),
160
+ });
161
+ }
162
+ },
163
+ );
164
+ return () => {
165
+ cancelled = true;
166
+ controller?.abort();
167
+ };
168
+ }, [input, needsLoad, attempt, enabled]);
169
+
170
+ return React.useMemo(() => {
171
+ if (!needsLoad) {
172
+ return {
173
+ data: input as EmojiData | undefined,
174
+ loading: false,
175
+ error: null,
176
+ retry,
177
+ };
178
+ }
179
+ if (loaded && loaded.source === input && loaded.attempt === attempt) {
180
+ return { data: loaded.data, loading: false, error: loaded.error, retry };
181
+ }
182
+ return { data: EMPTY_EMOJI_DATA, loading: true, error: null, retry };
183
+ }, [input, needsLoad, loaded, attempt, retry]);
184
+ }
185
+
186
+ const DataLoadingContext = /* @__PURE__ */ React.createContext<EmojiDataState>({
187
+ loading: false,
188
+ error: null,
189
+ retry: () => {},
190
+ });
191
+
192
+ export const DataLoadingProvider = DataLoadingContext.Provider;
193
+
194
+ export function useIsEmojiDataLoading(): boolean {
195
+ return React.useContext(DataLoadingContext).loading;
196
+ }
197
+
198
+ export function useEmojiDataState(): EmojiDataState {
199
+ return React.useContext(DataLoadingContext);
200
+ }
@@ -1,29 +1,91 @@
1
+ import * as React from 'react';
2
+
3
+ import { ClassNames } from '../DomUtils/classNames';
1
4
  import { scrollTo } from '../DomUtils/scrollTo';
2
5
  import { NullableElement } from '../DomUtils/selectors';
3
6
  import {
4
7
  useBodyRef,
5
8
  usePickerMainRef,
9
+ useSearchInputRef,
6
10
  } from '../components/context/ElementRefContext';
11
+ import {
12
+ useNavigationRegistry,
13
+ useSearchTermState,
14
+ } from '../components/context/PickerContext';
15
+ import { normalizeQuery } from '../data-core/prepare';
16
+
17
+ // A tab clicked right after editing the search (e.g. clearing it) lands
18
+ // before the debounced filter commit: sections are still collapsed or
19
+ // hidden, so the target's offset is about to move. Wait until the committed
20
+ // search matches the input and the target is laid out, with a safety cap;
21
+ // a newer jump supersedes this one.
22
+ const MAX_WAIT_MS = 3000;
23
+
24
+ // CSS.escape is missing in some embedded WebViews and older test DOMs; a
25
+ // quoted attribute value only needs backslashes and quotes escaped.
26
+ function escapeAttributeValue(value: string): string {
27
+ return typeof CSS !== 'undefined' && CSS.escape
28
+ ? CSS.escape(value)
29
+ : value.replace(/["\\]/g, '\\$&');
30
+ }
31
+
32
+ function searchInputValue(input: HTMLInputElement | null): string | undefined {
33
+ return input ? normalizeQuery(input.value) : undefined;
34
+ }
7
35
 
8
36
  export function useScrollCategoryIntoView() {
9
37
  const BodyRef = useBodyRef();
10
38
  const PickerMainRef = usePickerMainRef();
39
+ const SearchInputRef = useSearchInputRef();
40
+ const registry = useNavigationRegistry();
41
+ const [searchTerm] = useSearchTermState();
42
+ const committedSearch = React.useRef(searchTerm);
43
+ committedSearch.current = searchTerm;
11
44
 
12
45
  return function scrollCategoryIntoView(category: string): void {
13
- if (!BodyRef.current) {
14
- return;
15
- }
16
- // Group names are user-controlled; escape for the attribute selector.
17
- const $category = BodyRef.current?.querySelector(
18
- `[data-name="${CSS.escape(category)}"]`,
19
- ) as NullableElement;
46
+ // An explicit jump supersedes pending scroll/focus work (e.g. the
47
+ // post-search scroll to top).
48
+ registry.invalidate();
49
+ const generation = registry.currentGeneration();
50
+ const deadline = Date.now() + MAX_WAIT_MS;
20
51
 
21
- if (!$category) {
22
- return;
23
- }
52
+ const attempt = () => {
53
+ if (!registry.isCurrent(generation) || !BodyRef.current) {
54
+ return;
55
+ }
56
+ // Group names are user-controlled; escape for the attribute selector.
57
+ const $category = BodyRef.current.querySelector(
58
+ `[data-epr-category="${escapeAttributeValue(category)}"]`,
59
+ ) as NullableElement;
24
60
 
25
- const offsetTop = $category.offsetTop || 0;
61
+ if (!$category) {
62
+ return;
63
+ }
64
+ // Search and SearchInput share this native ref. A wrapper selector
65
+ // misses BYOD inputs, and raw lowercase text does not match the
66
+ // trimmed/lowercased query actually committed by useFilter.
67
+ const typed = searchInputValue(SearchInputRef.current);
68
+ const searchPending =
69
+ typed !== undefined && typed !== (committedSearch.current || '');
70
+ const unsettled =
71
+ searchPending || $category.classList.contains(ClassNames.hidden);
72
+ if (unsettled && Date.now() < deadline) {
73
+ requestAnimationFrame(attempt);
74
+ return;
75
+ }
26
76
 
27
- scrollTo(PickerMainRef.current, offsetTop);
77
+ // Removing the filter restores section presence before the height
78
+ // measurement effects update preceding rows. Read the offset on the
79
+ // next frame, after those updates, rather than retaining the short
80
+ // search-result layout as the jump destination.
81
+ scrollTo(
82
+ PickerMainRef.current,
83
+ () => $category.offsetTop,
84
+ () =>
85
+ registry.isCurrent(generation) &&
86
+ !!BodyRef.current?.contains($category),
87
+ );
88
+ };
89
+ attempt();
28
90
  };
29
91
  }
@@ -0,0 +1,369 @@
1
+ import * as React from 'react';
2
+
3
+ import { useSearchInputRef } from '../components/context/ElementRefContext';
4
+ import {
5
+ useNavigationRegistry,
6
+ useSearchCommittedState,
7
+ useSearchComposingState,
8
+ useSearchDisplayState,
9
+ } from '../components/context/PickerContext';
10
+ import { useMutableConfig } from '../config/mutableConfig';
11
+ import {
12
+ useSearchDisabledConfig,
13
+ useSearchValueConfig,
14
+ } from '../config/useConfig';
15
+
16
+ import { useFilter } from './useFilter';
17
+ import { useFocusSearchInput } from './useFocus';
18
+
19
+ // Controlled/uncontrolled search transition.
20
+ //
21
+ // - The visible/callback value is raw user text; filtering derives a
22
+ // normalized query and is debounced 100 ms through the shared
23
+ // accepted-search service (`useFilter().onChange`).
24
+ // - Controlled (`searchValue` present): user edits emit an
25
+ // `onSearchChange(proposal)` synchronously and commit nothing locally —
26
+ // exactly like an ordinary controlled `<input>`. Filtering is scheduled
27
+ // only when the parent supplies a new accepted `searchValue` (see
28
+ // `SearchSync` in PickerMainBehaviors); rejected proposals schedule nothing.
29
+ // - Uncontrolled: edits commit immediately and emit when `onSearchChange`
30
+ // is present; `defaultSearchValue` is the initial accepted value.
31
+ // - IME composition buffers in display state only; nothing commits, emits,
32
+ // or schedules until `compositionend`, which finalizes exactly once.
33
+ // - With Search omitted/disabled, type-to-search is a no-op that leaves
34
+ // Grid focus and search state unchanged; an explicit controlled
35
+ // `searchValue` still filters through the provider-level sync.
36
+
37
+ export function useIsControlledSearch(): boolean {
38
+ return useSearchValueConfig() !== undefined;
39
+ }
40
+
41
+ /** Accepted raw value: the controlled prop, or the committed value. */
42
+ export function useAcceptedSearchValue(): string {
43
+ const searchValue = useSearchValueConfig();
44
+ const [committed] = useSearchCommittedState();
45
+ return searchValue ?? committed;
46
+ }
47
+
48
+ function useEmitSearchChange(): (value: string) => void {
49
+ const { current } = useMutableConfig();
50
+ return React.useCallback(
51
+ (value: string) => {
52
+ current.onSearchChange?.(value);
53
+ },
54
+ [current],
55
+ );
56
+ }
57
+
58
+ /**
59
+ * Rejected-proposal reconciliation for controlled search.
60
+ *
61
+ * A rejected proposal must not persist visibly: the browser keeps the
62
+ * native keystroke (React never rewrites an unchanged value prop), so the
63
+ * library reconciles the input back to the latest accepted value once
64
+ * typing goes quiet — unless the parent accepted/transformed it meanwhile
65
+ * (then reconciling is a no-op), the input unmounted, or a composition
66
+ * owns it. This makes an ignoring parent eventually show the prop rather
67
+ * than leaving an optimistic value visible.
68
+ *
69
+ * The 500 ms quiet delay is independent of filtering's 100 ms debounce.
70
+ * It lets ordinary typing bursts accumulate: intermediate keystrokes arrive
71
+ * before it fires. See also pendingProposals below.
72
+ */
73
+ const RECONCILE_AFTER_QUIET_MS = 500;
74
+
75
+ interface PendingProposal {
76
+ proposal: string;
77
+ accepted: string;
78
+ }
79
+
80
+ // Last controlled emission per Root, stamped with the accepted value at
81
+ // emit time. Lets a grid keystroke build on an in-flight proposal when
82
+ // the parent commit lags behind machine-speed typing, while an
83
+ // acceptance-stamp mismatch always falls back to the accepted value.
84
+ const pendingProposals = new WeakMap<
85
+ object,
86
+ { current: PendingProposal | null }
87
+ >();
88
+
89
+ function pendingFor(registry: object): { current: PendingProposal | null } {
90
+ let ref = pendingProposals.get(registry);
91
+ if (!ref) {
92
+ ref = { current: null };
93
+ pendingProposals.set(registry, ref);
94
+ }
95
+ return ref;
96
+ }
97
+
98
+ function usePendingProposal() {
99
+ const registry = useNavigationRegistry();
100
+ const searchValue = useSearchValueConfig();
101
+ const record = React.useCallback(
102
+ (proposal: string | null) => {
103
+ pendingFor(registry).current =
104
+ proposal === null ? null : { proposal, accepted: searchValue ?? '' };
105
+ },
106
+ [registry, searchValue],
107
+ );
108
+ const read = React.useCallback(
109
+ () => pendingFor(registry).current,
110
+ [registry],
111
+ );
112
+ return { record, read };
113
+ }
114
+
115
+ function useControlledReconcile() {
116
+ const searchValue = useSearchValueConfig();
117
+ const [composing] = useSearchComposingState();
118
+ const [, setDisplay] = useSearchDisplayState();
119
+ const acceptedRef = React.useRef(searchValue ?? '');
120
+ acceptedRef.current = searchValue ?? '';
121
+ const composingRef = React.useRef(composing);
122
+ composingRef.current = composing;
123
+ const timerRef = React.useRef<ReturnType<typeof setTimeout> | null>(null);
124
+ const registry = useNavigationRegistry();
125
+
126
+ React.useEffect(
127
+ () => () => {
128
+ if (timerRef.current !== null) {
129
+ clearTimeout(timerRef.current);
130
+ }
131
+ },
132
+ [],
133
+ );
134
+
135
+ return React.useCallback(() => {
136
+ if (timerRef.current !== null) {
137
+ clearTimeout(timerRef.current);
138
+ }
139
+ timerRef.current = setTimeout(() => {
140
+ timerRef.current = null;
141
+ const pending = pendingFor(registry).current;
142
+ pendingFor(registry).current = null;
143
+ if (composingRef.current) {
144
+ return;
145
+ }
146
+ // Wipe only a still-outstanding proposal: if the parent moved on
147
+ // (accepted/transformed), display sync already owns the value.
148
+ // Reconcile through React state, never by writing the DOM
149
+ // directly: a DOM-only write desyncs state from the input, and the
150
+ // dead proposal resurfaces on the next Search rerender.
151
+ if (pending !== null && pending.accepted === acceptedRef.current) {
152
+ setDisplay(acceptedRef.current);
153
+ }
154
+ }, RECONCILE_AFTER_QUIET_MS);
155
+ }, [registry, setDisplay]);
156
+ }
157
+ function useCommitSearch(): (raw: string) => void {
158
+ const [, setCommitted] = useSearchCommittedState();
159
+ const [, setDisplay] = useSearchDisplayState();
160
+ const { onChange: commitFilter } = useFilter();
161
+ const emit = useEmitSearchChange();
162
+
163
+ return React.useCallback(
164
+ (raw: string) => {
165
+ setCommitted(raw);
166
+ setDisplay(raw);
167
+ commitFilter(raw);
168
+ emit(raw);
169
+ },
170
+ [setCommitted, setDisplay, commitFilter, emit],
171
+ );
172
+ }
173
+
174
+ export function useSearchInputController() {
175
+ const searchValue = useSearchValueConfig();
176
+ const isControlled = searchValue !== undefined;
177
+ const accepted = useAcceptedSearchValue();
178
+ const [display, setDisplay] = useSearchDisplayState();
179
+ const [composing, setComposing] = useSearchComposingState();
180
+ const commit = useCommitSearch();
181
+ const emit = useEmitSearchChange();
182
+ const reconcileControlled = useControlledReconcile();
183
+ const { record: recordProposal } = usePendingProposal();
184
+ // Same-task echo of a just-finalized composition: the
185
+ // browser reports the committed text through a trailing input event
186
+ // after compositionend already emitted and synced it. Treating that
187
+ // echo as a fresh proposal would resurrect a dead value into display
188
+ // and duplicate the emission, so the controlled transition swallows an
189
+ // input event carrying exactly the finalized text while this marker
190
+ // stands. It clears on a microtask, so only same-task echoes match —
191
+ // any later genuine edit (even identical pasted text) takes the normal
192
+ // path, since user input always arrives in a later task.
193
+ const compositionEchoRef = React.useRef<string | null>(null);
194
+ const displayRef = React.useRef(display);
195
+ displayRef.current = display;
196
+
197
+ // Reconcile display with the accepted value whenever it changes
198
+ // externally — but never while an IME composition owns the DOM value.
199
+ // Draft edits must not trigger this effect: they reconcile only after
200
+ // the quiet window. Read the current display through a ref instead.
201
+ // The equality guard matters: an unconditional setDisplay schedules a
202
+ // second commit per keystroke (the update bails out, but Profiler and
203
+ // cascading-effect checks still observe it), doubling render work.
204
+ React.useEffect(() => {
205
+ if (!composing && accepted !== displayRef.current) {
206
+ setDisplay(accepted);
207
+ }
208
+ }, [accepted, composing, setDisplay]);
209
+
210
+ function handleChange(event: React.ChangeEvent<HTMLInputElement>) {
211
+ const raw = event.target.value;
212
+ const nativeComposing =
213
+ (event.nativeEvent as InputEvent).isComposing === true;
214
+ if (composing || nativeComposing) {
215
+ // IME draft: visible only, no commit/emit/schedule.
216
+ setDisplay(raw);
217
+ return;
218
+ }
219
+ if (isControlled) {
220
+ if (
221
+ compositionEchoRef.current !== null &&
222
+ compositionEchoRef.current === raw
223
+ ) {
224
+ // Trailing echo of the finalized composition: already emitted
225
+ // and synced by compositionend; not a new proposal.
226
+ return;
227
+ }
228
+ // Emit the proposal; the parent decides what becomes accepted.
229
+ // Display tracks the proposal immediately (React-managed, so parent
230
+ // acceptance synchronizes it immediately); rejections reconcile
231
+ // back to the prop once typing goes quiet.
232
+ setDisplay(raw);
233
+ emit(raw);
234
+ recordProposal(raw);
235
+ reconcileControlled();
236
+ return;
237
+ }
238
+ commit(raw);
239
+ }
240
+
241
+ function handleCompositionStart() {
242
+ setComposing(true);
243
+ }
244
+
245
+ function handleCompositionEnd(
246
+ event: React.CompositionEvent<HTMLInputElement>,
247
+ ) {
248
+ const raw = event.currentTarget.value;
249
+ setComposing(false);
250
+ if (isControlled) {
251
+ emit(raw);
252
+ recordProposal(null);
253
+ // Reconcile to whatever the parent supplies, exactly as an ordinary
254
+ // controlled input would after composition.
255
+ setDisplay(searchValue ?? '');
256
+ compositionEchoRef.current = raw;
257
+ queueMicrotask(() => {
258
+ if (compositionEchoRef.current === raw) {
259
+ compositionEchoRef.current = null;
260
+ }
261
+ });
262
+ return;
263
+ }
264
+ commit(raw);
265
+ }
266
+
267
+ return {
268
+ value: display,
269
+ handleChange,
270
+ handleCompositionStart,
271
+ handleCompositionEnd,
272
+ isControlled,
273
+ };
274
+ }
275
+
276
+ /** Grid type-to-search transition. */
277
+ export function useTypeToSearchKey() {
278
+ const searchValue = useSearchValueConfig();
279
+ const isControlled = searchValue !== undefined;
280
+ const [committed] = useSearchCommittedState();
281
+ const SearchInputRef = useSearchInputRef();
282
+ const searchDisabled = useSearchDisabledConfig();
283
+ const commit = useCommitSearch();
284
+ const emit = useEmitSearchChange();
285
+ const reconcileControlled = useControlledReconcile();
286
+ const [, setDisplay] = useSearchDisplayState();
287
+ const { record: recordProposal, read: readProposal } = usePendingProposal();
288
+
289
+ return React.useCallback(
290
+ (key: string) => {
291
+ const input = SearchInputRef.current;
292
+ if (searchDisabled || !input || input.disabled || input.readOnly) {
293
+ return;
294
+ }
295
+ // Focus transfers synchronously so the very next keystroke is
296
+ // already an ordinary input edit — machine-speed bursts must not
297
+ // outrun a deferred focus.
298
+ input.focus();
299
+ if (isControlled) {
300
+ // Build on the in-flight proposal when the parent commit lags;
301
+ // fall back to the accepted value whenever the parent moved on
302
+ // (accepted, transformed, cleared, or composed since).
303
+ const accepted = searchValue ?? '';
304
+ const pending = readProposal();
305
+ const base =
306
+ pending !== null && pending.accepted === accepted
307
+ ? pending.proposal
308
+ : accepted;
309
+ // Seed the input DOM with the proposal: the grid keystroke never
310
+ // entered the input natively, so without this the next key would
311
+ // compute from a stale value whenever the parent commit lags.
312
+ // This is transient view state, not accepted state — a rejection
313
+ // still reconciles back to the prop once typing goes quiet, and
314
+ // filtering follows only if the parent accepts the proposal.
315
+ const proposal = `${base}${key}`;
316
+ input.value = proposal;
317
+ setDisplay(proposal);
318
+ emit(proposal);
319
+ recordProposal(proposal);
320
+ reconcileControlled();
321
+ return;
322
+ }
323
+ commit(`${committed}${key}`);
324
+ },
325
+ [
326
+ searchDisabled,
327
+ SearchInputRef,
328
+ isControlled,
329
+ searchValue,
330
+ committed,
331
+ commit,
332
+ emit,
333
+ readProposal,
334
+ recordProposal,
335
+ reconcileControlled,
336
+ setDisplay,
337
+ ],
338
+ );
339
+ }
340
+
341
+ /** Explicit programmatic proposals; no input or input-controller effects required. */
342
+ export function useSetSearchValue() {
343
+ const controlled = useIsControlledSearch();
344
+ const commit = useCommitSearch();
345
+ const emit = useEmitSearchChange();
346
+ return React.useCallback(
347
+ (value: string) => {
348
+ if (controlled) emit(value);
349
+ else commit(value);
350
+ },
351
+ [controlled, commit, emit],
352
+ );
353
+ }
354
+
355
+ /** Clear-button / Escape transition: proposes/commits ''. */
356
+ export function useClearSearchValue() {
357
+ const SearchInputRef = useSearchInputRef();
358
+ const focusSearchInput = useFocusSearchInput();
359
+ const setValue = useSetSearchValue();
360
+
361
+ return React.useCallback(() => {
362
+ setValue('');
363
+ if (SearchInputRef.current) {
364
+ // The managed input follows accepted state. A controlled parent may
365
+ // reject the empty proposal, so clearing must not mutate the DOM.
366
+ focusSearchInput();
367
+ }
368
+ }, [setValue, SearchInputRef, focusSearchInput]);
369
+ }