emoji-picker-react 4.22.3 → 5.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +331 -49
- package/dist/DomUtils/categoryNameFromDom.d.ts +1 -1
- package/dist/DomUtils/classNames.d.ts +23 -23
- package/dist/DomUtils/detectEmojyPartiallyBelowFold.d.ts +2 -2
- package/dist/DomUtils/elementPositionInRow.d.ts +5 -5
- package/dist/DomUtils/eventBelongsToPicker.d.ts +2 -0
- package/dist/DomUtils/focusElement.d.ts +5 -5
- package/dist/DomUtils/getActiveElement.d.ts +2 -2
- package/dist/DomUtils/isJsdom.d.ts +1 -0
- package/dist/DomUtils/isRtl.d.ts +1 -0
- package/dist/DomUtils/keyboardNavigation.d.ts +15 -8
- package/dist/DomUtils/scrollTo.d.ts +4 -5
- package/dist/DomUtils/selectors.d.ts +31 -35
- package/dist/EmojiPickerReact.d.ts +5 -5
- package/dist/Stylesheet/DedupedStyle.d.ts +13 -0
- package/dist/Stylesheet/stylesheet.d.ts +40 -37
- package/dist/components/ErrorBoundary.d.ts +15 -15
- package/dist/components/Layout/Absolute.d.ts +8 -8
- package/dist/components/Layout/Flex.d.ts +13 -13
- package/dist/components/Layout/Relative.d.ts +8 -8
- package/dist/components/Layout/Space.d.ts +7 -7
- package/dist/components/Reactions/BtnPlus.d.ts +2 -2
- package/dist/components/Reactions/DEFAULT_REACTIONS.d.ts +1 -1
- package/dist/components/Reactions/Reactions.d.ts +3 -2
- package/dist/components/atoms/Button.d.ts +11 -6
- package/dist/components/body/EmojiCategory.d.ts +13 -12
- package/dist/components/body/EmojiList.d.ts +7 -4
- package/dist/components/body/EmojiVariationPicker.d.ts +2 -2
- package/dist/components/body/MeasureEmoji.d.ts +2 -2
- package/dist/components/body/listComponents.d.ts +47 -0
- package/dist/components/context/ElementRefContext.d.ts +18 -16
- package/dist/components/context/PickerConfigContext.d.ts +14 -9
- package/dist/components/context/PickerContext.d.ts +57 -25
- package/dist/components/context/PickerDataContext.d.ts +23 -17
- package/dist/components/emoji/BaseEmojiProps.d.ts +14 -13
- package/dist/components/emoji/ClickableEmojiButton.d.ts +23 -15
- package/dist/components/emoji/Emoji.d.ts +18 -15
- package/dist/components/emoji/EmojiImg.d.ts +11 -11
- package/dist/components/emoji/ExportedEmoji.d.ts +11 -11
- package/dist/components/emoji/NativeEmoji.d.ts +6 -6
- package/dist/components/emoji/ViewOnlyEmoji.d.ts +3 -3
- package/dist/components/emoji/emojiStyles.d.ts +6 -1
- package/dist/components/footer/Preview.d.ts +7 -7
- package/dist/components/header/Header.d.ts +2 -2
- package/dist/components/header/Search/BtnClearSearch.d.ts +2 -2
- package/dist/components/header/Search/IcnSearch.d.ts +2 -2
- package/dist/components/header/Search/Search.d.ts +11 -3
- package/dist/components/header/SkinTonePicker/BtnSkinToneVariation.d.ts +12 -12
- package/dist/components/header/SkinTonePicker/SkinTonePicker.d.ts +11 -11
- package/dist/components/icons/svgIcons.d.ts +4 -0
- package/dist/components/main/PickerMainBehaviors.d.ts +4 -0
- package/dist/components/main/defaultAppearance.d.ts +12 -0
- package/dist/components/main/labelHeight.d.ts +1 -0
- package/dist/components/navigation/CategoryButton.d.ts +12 -12
- package/dist/components/navigation/CategoryNavIcon.d.ts +4 -4
- package/dist/components/navigation/CategoryNavigation.d.ts +19 -2
- package/dist/config/categoryConfig.d.ts +34 -34
- package/dist/config/cdnUrls.d.ts +2 -2
- package/dist/config/compareConfig.d.ts +10 -2
- package/dist/config/config.d.ts +166 -64
- package/dist/config/customEmojiConfig.d.ts +14 -14
- package/dist/config/defaultPickerConfiguration.d.ts +10 -0
- package/dist/config/mutableConfig.d.ts +32 -10
- package/dist/config/useConfig.d.ts +48 -36
- package/dist/data/defaultEmojiData.d.ts +9 -0
- package/dist/data/defaultEmojiData.js +40 -0
- package/dist/data/emojis-bn.d.mts +39 -0
- package/dist/data/emojis-bn.d.ts +38 -2
- package/dist/data/emojis-bn.js +25 -2
- package/dist/data/emojis-bn.mjs +2 -0
- package/dist/data/emojis-da.d.mts +39 -0
- package/dist/data/emojis-da.d.ts +38 -2
- package/dist/data/emojis-da.js +25 -2
- package/dist/data/emojis-da.mjs +2 -0
- package/dist/data/emojis-de.d.mts +39 -0
- package/dist/data/emojis-de.d.ts +38 -2
- package/dist/data/emojis-de.js +25 -2
- package/dist/data/emojis-de.mjs +2 -0
- package/dist/data/emojis-en-gb.d.mts +39 -0
- package/dist/data/emojis-en-gb.d.ts +38 -2
- package/dist/data/emojis-en-gb.js +25 -2
- package/dist/data/emojis-en-gb.mjs +2 -0
- package/dist/data/emojis-en.d.mts +39 -0
- package/dist/data/emojis-en.d.ts +38 -2
- package/dist/data/emojis-en.js +25 -2
- package/dist/data/emojis-en.mjs +2 -0
- package/dist/data/emojis-es-mx.d.mts +39 -0
- package/dist/data/emojis-es-mx.d.ts +38 -2
- package/dist/data/emojis-es-mx.js +25 -2
- package/dist/data/emojis-es-mx.mjs +2 -0
- package/dist/data/emojis-es.d.mts +39 -0
- package/dist/data/emojis-es.d.ts +38 -2
- package/dist/data/emojis-es.js +25 -2
- package/dist/data/emojis-es.mjs +2 -0
- package/dist/data/emojis-et.d.mts +39 -0
- package/dist/data/emojis-et.d.ts +38 -2
- package/dist/data/emojis-et.js +25 -2
- package/dist/data/emojis-et.mjs +2 -0
- package/dist/data/emojis-fi.d.mts +39 -0
- package/dist/data/emojis-fi.d.ts +38 -2
- package/dist/data/emojis-fi.js +25 -2
- package/dist/data/emojis-fi.mjs +2 -0
- package/dist/data/emojis-fr.d.mts +39 -0
- package/dist/data/emojis-fr.d.ts +38 -2
- package/dist/data/emojis-fr.js +25 -2
- package/dist/data/emojis-fr.mjs +2 -0
- package/dist/data/emojis-hi.d.mts +39 -0
- package/dist/data/emojis-hi.d.ts +38 -2
- package/dist/data/emojis-hi.js +25 -2
- package/dist/data/emojis-hi.mjs +2 -0
- package/dist/data/emojis-hu.d.mts +39 -0
- package/dist/data/emojis-hu.d.ts +38 -2
- package/dist/data/emojis-hu.js +25 -2
- package/dist/data/emojis-hu.mjs +2 -0
- package/dist/data/emojis-it.d.mts +39 -0
- package/dist/data/emojis-it.d.ts +38 -2
- package/dist/data/emojis-it.js +25 -2
- package/dist/data/emojis-it.mjs +2 -0
- package/dist/data/emojis-ja.d.mts +39 -0
- package/dist/data/emojis-ja.d.ts +38 -2
- package/dist/data/emojis-ja.js +25 -2
- package/dist/data/emojis-ja.mjs +2 -0
- package/dist/data/emojis-ko.d.mts +39 -0
- package/dist/data/emojis-ko.d.ts +38 -2
- package/dist/data/emojis-ko.js +25 -2
- package/dist/data/emojis-ko.mjs +2 -0
- package/dist/data/emojis-lt.d.mts +39 -0
- package/dist/data/emojis-lt.d.ts +38 -2
- package/dist/data/emojis-lt.js +25 -2
- package/dist/data/emojis-lt.mjs +2 -0
- package/dist/data/emojis-ms.d.mts +39 -0
- package/dist/data/emojis-ms.d.ts +38 -2
- package/dist/data/emojis-ms.js +25 -2
- package/dist/data/emojis-ms.mjs +2 -0
- package/dist/data/emojis-nb.d.mts +39 -0
- package/dist/data/emojis-nb.d.ts +38 -2
- package/dist/data/emojis-nb.js +25 -2
- package/dist/data/emojis-nb.mjs +2 -0
- package/dist/data/emojis-nl.d.mts +39 -0
- package/dist/data/emojis-nl.d.ts +38 -2
- package/dist/data/emojis-nl.js +25 -2
- package/dist/data/emojis-nl.mjs +2 -0
- package/dist/data/emojis-pl.d.mts +39 -0
- package/dist/data/emojis-pl.d.ts +38 -2
- package/dist/data/emojis-pl.js +25 -2
- package/dist/data/emojis-pl.mjs +2 -0
- package/dist/data/emojis-pt.d.mts +39 -0
- package/dist/data/emojis-pt.d.ts +38 -2
- package/dist/data/emojis-pt.js +25 -2
- package/dist/data/emojis-pt.mjs +2 -0
- package/dist/data/emojis-ru.d.mts +39 -0
- package/dist/data/emojis-ru.d.ts +38 -2
- package/dist/data/emojis-ru.js +25 -2
- package/dist/data/emojis-ru.mjs +2 -0
- package/dist/data/emojis-sv.d.mts +39 -0
- package/dist/data/emojis-sv.d.ts +38 -2
- package/dist/data/emojis-sv.js +25 -2
- package/dist/data/emojis-sv.mjs +2 -0
- package/dist/data/emojis-th.d.mts +39 -0
- package/dist/data/emojis-th.d.ts +38 -2
- package/dist/data/emojis-th.js +25 -2
- package/dist/data/emojis-th.mjs +2 -0
- package/dist/data/emojis-uk.d.mts +39 -0
- package/dist/data/emojis-uk.d.ts +38 -2
- package/dist/data/emojis-uk.js +25 -2
- package/dist/data/emojis-uk.mjs +2 -0
- package/dist/data/emojis-vi.d.mts +39 -0
- package/dist/data/emojis-vi.d.ts +38 -2
- package/dist/data/emojis-vi.js +25 -2
- package/dist/data/emojis-vi.mjs +2 -0
- package/dist/data/emojis-zh-hant.d.mts +39 -0
- package/dist/data/emojis-zh-hant.d.ts +38 -2
- package/dist/data/emojis-zh-hant.js +25 -2
- package/dist/data/emojis-zh-hant.mjs +2 -0
- package/dist/data/emojis-zh.d.mts +39 -0
- package/dist/data/emojis-zh.d.ts +38 -2
- package/dist/data/emojis-zh.js +25 -2
- package/dist/data/emojis-zh.mjs +2 -0
- package/dist/data/index.js +1 -0
- package/dist/data/registerDefaultEmojiData.d.ts +1 -0
- package/dist/data/registerDefaultEmojiData.js +3 -0
- package/dist/data/skinToneVariations.js +2 -2
- package/dist/data/skinToneVariations.ts +8 -7
- package/dist/data-core/pickerData.d.ts +12 -0
- package/dist/data-core/prepare.d.ts +23 -0
- package/dist/data-core/search.d.ts +13 -0
- package/dist/data-core/types.d.ts +23 -0
- package/dist/data.d.mts +72 -0
- package/dist/data.d.ts +72 -0
- package/dist/dataUtils/DataTypes.d.ts +23 -19
- package/dist/dataUtils/emojiSelectors.d.ts +15 -16
- package/dist/dataUtils/emojiUtils.d.ts +1 -12
- package/dist/dataUtils/nativeEmojiSequenceSupport.d.ts +10 -0
- package/dist/dataUtils/nativeEmojiSupport.d.ts +40 -0
- package/dist/dataUtils/parseNativeEmoji.d.ts +1 -1
- package/dist/dataUtils/suggested.d.ts +11 -11
- package/dist/dataUtils/suggestedEmojis.d.ts +7 -0
- package/dist/esm/chunks/chunk-7ZGTUZFB.mjs +8025 -0
- package/dist/esm/chunks/chunk-ACXAQEKG.mjs +149 -0
- package/dist/esm/chunks/chunk-F2KS7DCA.mjs +6735 -0
- package/dist/esm/chunks/chunk-L5W4PFRJ.mjs +14 -0
- package/dist/esm/chunks/chunk-LO4TPPLH.mjs +33 -0
- package/dist/esm/chunks/emojis-6L3GRBAP.mjs +7 -0
- package/dist/esm/data.mjs +11 -0
- package/dist/esm/index.mjs +279 -0
- package/dist/esm/primitives/index.mjs +8 -0
- package/dist/hooks/preloadEmoji.d.ts +5 -9
- package/dist/hooks/useActiveCategoryScrollDetection.d.ts +11 -11
- package/dist/hooks/useCategoryHeight.d.ts +7 -5
- package/dist/hooks/useCloseAllOpenToggles.d.ts +2 -2
- package/dist/hooks/useDataIdentityStabilityWarning.d.ts +13 -0
- package/dist/hooks/useDebouncedState.d.ts +1 -1
- package/dist/hooks/useDisallowMouseMove.d.ts +4 -4
- package/dist/hooks/useDisallowedEmojis.d.ts +2 -3
- package/dist/hooks/useEmojiPreviewEvents.d.ts +3 -3
- package/dist/hooks/useEmojiVirtualization.d.ts +22 -17
- package/dist/hooks/useFilter.d.ts +10 -16
- package/dist/hooks/useFocus.d.ts +3 -3
- package/dist/hooks/useInitialLoad.d.ts +2 -2
- package/dist/hooks/useIsEmojiHidden.d.ts +8 -8
- package/dist/hooks/useIsMounted.d.ts +1 -1
- package/dist/hooks/useIsSearchMode.d.ts +1 -1
- package/dist/hooks/useIsomorphicLayoutEffect.d.ts +2 -2
- package/dist/hooks/useKeyboardNavigation.d.ts +3 -1
- package/dist/hooks/useMouseDownHandlers.d.ts +8 -4
- package/dist/hooks/useOnFocus.d.ts +1 -1
- package/dist/hooks/useOnScroll.d.ts +2 -2
- package/dist/hooks/useReactionsFocus.d.ts +1 -0
- package/dist/hooks/useRegisterRegion.d.ts +24 -0
- package/dist/hooks/useResolvedEmojiData.d.ts +37 -0
- package/dist/hooks/useScrollCategoryIntoView.d.ts +1 -1
- package/dist/hooks/useSearchController.d.ts +17 -0
- package/dist/hooks/useSearchResults.d.ts +8 -0
- package/dist/hooks/useSetVariationPicker.d.ts +2 -2
- package/dist/hooks/useShouldHideCustomEmojis.d.ts +1 -1
- package/dist/hooks/useShouldShowSkinTonePicker.d.ts +4 -4
- package/dist/index.d.mts +376 -0
- package/dist/index.d.ts +376 -9
- package/dist/index.js +2 -8
- package/dist/primitives/CategoryNav.d.ts +4 -0
- package/dist/primitives/Empty.d.ts +6 -0
- package/dist/primitives/List.d.ts +4 -0
- package/dist/primitives/LoadError.d.ts +8 -0
- package/dist/primitives/Loading.d.ts +4 -0
- package/dist/primitives/Panel.d.ts +3 -0
- package/dist/primitives/Preview.d.ts +3 -0
- package/dist/primitives/Reactions.d.ts +2 -0
- package/dist/primitives/Root.d.ts +26 -0
- package/dist/primitives/Search.d.ts +5 -0
- package/dist/primitives/SearchInput.d.ts +3 -0
- package/dist/primitives/SkinTone.d.ts +4 -0
- package/dist/primitives/Viewport.d.ts +19 -0
- package/dist/primitives/appearance.d.ts +3 -0
- package/dist/primitives/components.d.ts +27 -0
- package/dist/primitives/hooks.d.ts +48 -0
- package/dist/primitives/index.d.mts +561 -0
- package/dist/primitives/index.d.ts +561 -0
- package/dist/primitives/index.js +2 -0
- package/dist/primitives/nativeProps.d.ts +15 -0
- package/dist/primitives/scope.d.ts +13 -0
- package/dist/primitives/structuralStyles.d.ts +16 -0
- package/dist/primitives/tokens.d.ts +18 -0
- package/dist/primitives/types.d.ts +85 -0
- package/dist/state/activeEmojiStore.d.ts +7 -0
- package/dist/state/navigationRegistry.d.ts +61 -0
- package/dist/state/regionTraversal.d.ts +20 -0
- package/dist/typeRefinements/typeRefinements.d.ts +6 -6
- package/dist/types/exposedTypes.d.ts +16 -6
- package/dist/types/exposedTypes.js +5 -0
- package/dist/virtualization/virtualizationHelpers.d.ts +22 -18
- package/llms-full.txt +3683 -0
- package/llms.txt +66 -480
- package/package.json +180 -66
- package/src/DomUtils/categoryNameFromDom.ts +1 -1
- package/src/DomUtils/classNames.ts +1 -4
- package/src/DomUtils/elementPositionInRow.ts +28 -6
- package/src/DomUtils/eventBelongsToPicker.ts +17 -0
- package/src/DomUtils/getActiveElement.ts +7 -1
- package/src/DomUtils/isJsdom.ts +12 -0
- package/src/DomUtils/isRtl.ts +5 -0
- package/src/DomUtils/keyboardNavigation.ts +164 -144
- package/src/DomUtils/scrollTo.ts +20 -18
- package/src/DomUtils/selectors.ts +30 -127
- package/src/EmojiPickerReact.tsx +155 -48
- package/src/Stylesheet/DedupedStyle.tsx +80 -0
- package/src/Stylesheet/stylesheet.tsx +128 -59
- package/src/components/Layout/Flex.tsx +12 -11
- package/src/components/Reactions/BtnPlus.tsx +61 -51
- package/src/components/Reactions/Reactions.tsx +113 -55
- package/src/components/atoms/Button.tsx +18 -15
- package/src/components/body/EmojiCategory.tsx +77 -40
- package/src/components/body/EmojiList.tsx +107 -44
- package/src/components/body/EmojiVariationPicker.tsx +113 -66
- package/src/components/body/MeasureEmoji.tsx +9 -2
- package/src/components/body/listComponents.tsx +79 -0
- package/src/components/context/ElementRefContext.tsx +46 -35
- package/src/components/context/PickerConfigContext.tsx +55 -40
- package/src/components/context/PickerContext.tsx +567 -92
- package/src/components/context/PickerDataContext.tsx +114 -99
- package/src/components/emoji/BaseEmojiProps.ts +5 -4
- package/src/components/emoji/ClickableEmojiButton.tsx +148 -63
- package/src/components/emoji/Emoji.tsx +24 -2
- package/src/components/emoji/EmojiImg.tsx +13 -12
- package/src/components/emoji/ExportedEmoji.tsx +5 -4
- package/src/components/emoji/NativeEmoji.tsx +19 -16
- package/src/components/emoji/ViewOnlyEmoji.tsx +55 -30
- package/src/components/emoji/emojiStyles.ts +12 -11
- package/src/components/footer/Preview.tsx +86 -91
- package/src/components/header/Header.tsx +21 -6
- package/src/components/header/Search/BtnClearSearch.tsx +69 -55
- package/src/components/header/Search/IcnSearch.tsx +21 -21
- package/src/components/header/Search/Search.tsx +79 -137
- package/src/components/header/SkinTonePicker/BtnSkinToneVariation.tsx +99 -62
- package/src/components/header/SkinTonePicker/SkinTonePicker.tsx +84 -46
- package/src/components/icons/svgIcons.ts +17 -0
- package/src/components/main/PickerMainBehaviors.tsx +163 -0
- package/src/components/main/defaultAppearance.tsx +189 -0
- package/src/components/main/labelHeight.ts +5 -0
- package/src/components/navigation/CategoryButton.tsx +83 -57
- package/src/components/navigation/CategoryNavIcon.tsx +73 -194
- package/src/components/navigation/CategoryNavigation.tsx +118 -46
- package/src/config/categoryConfig.ts +7 -3
- package/src/config/cdnUrls.ts +11 -21
- package/src/config/compareConfig.ts +128 -30
- package/src/config/config.ts +228 -24
- package/src/config/defaultPickerConfiguration.ts +7 -0
- package/src/config/mutableConfig.ts +44 -20
- package/src/config/useConfig.ts +130 -37
- package/src/data/defaultEmojiData.ts +49 -0
- package/src/data/registerDefaultEmojiData.ts +8 -0
- package/src/data/skinToneVariations.ts +8 -7
- package/src/data-core/pickerData.ts +171 -0
- package/src/data-core/prepare.ts +163 -0
- package/src/data-core/search.ts +122 -0
- package/src/data-core/types.ts +36 -0
- package/src/data.ts +10 -0
- package/src/dataUtils/DataTypes.ts +4 -0
- package/src/dataUtils/emojiSelectors.ts +59 -63
- package/src/dataUtils/emojiUtils.ts +17 -74
- package/src/dataUtils/nativeEmojiSequenceSupport.ts +116 -0
- package/src/dataUtils/nativeEmojiSupport.ts +160 -0
- package/src/dataUtils/parseNativeEmoji.ts +3 -4
- package/src/dataUtils/suggested.ts +8 -4
- package/src/dataUtils/suggestedEmojis.ts +92 -0
- package/src/env.d.ts +4 -0
- package/src/hooks/preloadEmoji.ts +2 -47
- package/src/hooks/useActiveCategoryScrollDetection.ts +28 -16
- package/src/hooks/useCategoryHeight.ts +116 -10
- package/src/hooks/useDataIdentityStabilityWarning.ts +86 -0
- package/src/hooks/useDebouncedState.ts +13 -1
- package/src/hooks/useDisallowedEmojis.ts +31 -42
- package/src/hooks/useEmojiPreviewEvents.ts +43 -10
- package/src/hooks/useEmojiVirtualization.tsx +112 -52
- package/src/hooks/useFilter.ts +92 -132
- package/src/hooks/useFocus.ts +14 -24
- package/src/hooks/useIsEmojiHidden.ts +22 -14
- package/src/hooks/useKeyboardNavigation.ts +412 -432
- package/src/hooks/useMouseDownHandlers.ts +91 -16
- package/src/hooks/useOnFocus.ts +12 -3
- package/src/hooks/useOnScroll.ts +18 -2
- package/src/hooks/useReactionsFocus.ts +118 -0
- package/src/hooks/useRegisterRegion.ts +92 -0
- package/src/hooks/useResolvedEmojiData.ts +200 -0
- package/src/hooks/useScrollCategoryIntoView.ts +74 -12
- package/src/hooks/useSearchController.ts +369 -0
- package/src/hooks/useSearchResults.ts +51 -0
- package/src/hooks/useSetVariationPicker.ts +4 -1
- package/src/index.tsx +67 -5
- package/src/primitives/CategoryNav.tsx +40 -0
- package/src/primitives/Empty.tsx +68 -0
- package/src/primitives/List.tsx +61 -0
- package/src/primitives/LoadError.tsx +38 -0
- package/src/primitives/Loading.tsx +60 -0
- package/src/primitives/Panel.tsx +47 -0
- package/src/primitives/Preview.tsx +38 -0
- package/src/primitives/Reactions.tsx +2 -0
- package/src/primitives/Root.tsx +398 -0
- package/src/primitives/Search.tsx +41 -0
- package/src/primitives/SearchInput.tsx +110 -0
- package/src/primitives/SkinTone.tsx +76 -0
- package/src/primitives/Viewport.tsx +204 -0
- package/src/primitives/appearance.tsx +7 -0
- package/src/primitives/components.tsx +30 -0
- package/src/primitives/hooks.ts +165 -0
- package/src/primitives/index.ts +94 -0
- package/src/primitives/nativeProps.ts +90 -0
- package/src/primitives/scope.tsx +109 -0
- package/src/primitives/structuralStyles.tsx +136 -0
- package/src/primitives/tokens.ts +150 -0
- package/src/primitives/types.ts +192 -0
- package/src/state/activeEmojiStore.ts +39 -0
- package/src/state/navigationRegistry.ts +209 -0
- package/src/state/regionTraversal.ts +116 -0
- package/src/svg.d.ts +2 -2
- package/src/types/exposedTypes.ts +28 -1
- package/src/virtualization/virtualizationHelpers.ts +29 -8
- package/dist/components/body/Body.d.ts +0 -2
- package/dist/components/main/PickerMain.d.ts +0 -7
- package/dist/dataUtils/alphaNumericEmojiIndex.d.ts +0 -5
- package/dist/emoji-picker-react.cjs.development.js +0 -31413
- package/dist/emoji-picker-react.cjs.development.js.map +0 -1
- package/dist/emoji-picker-react.cjs.production.min.js +0 -2
- package/dist/emoji-picker-react.cjs.production.min.js.map +0 -1
- package/dist/emoji-picker-react.esm.js +0 -31411
- package/dist/emoji-picker-react.esm.js.map +0 -1
- package/dist/hooks/useHideEmojisByUniocode.d.ts +0 -1
- package/src/components/body/Body.tsx +0 -43
- package/src/components/main/PickerMain.tsx +0 -249
- 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
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
const
|
|
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
|
-
|
|
22
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
+
}
|