diff --git a/.changeset/fifty-candies-heal.md b/.changeset/fifty-candies-heal.md new file mode 100644 index 0000000000000..a67e4d5f376e5 --- /dev/null +++ b/.changeset/fifty-candies-heal.md @@ -0,0 +1,7 @@ +--- +'@rocket.chat/meteor': patch +--- + +fix: Buttons from emoji picker misbehaving on clicks + +An infinite render loop was preventing proper behavior when clicking on the emoji picker buttons. It was fixed by removing the unnecessary state update that was causing the loop and replacing multiple fires of the same mouseover event (when a mouseenter event was the right one to use). There is a chance this pre-existing bug was hidden by React 18's event delegation. diff --git a/apps/meteor/client/providers/EmojiPickerProvider/EmojiPickerProvider.spec.tsx b/apps/meteor/client/providers/EmojiPickerProvider/EmojiPickerProvider.spec.tsx new file mode 100644 index 0000000000000..5c16e1f8353db --- /dev/null +++ b/apps/meteor/client/providers/EmojiPickerProvider/EmojiPickerProvider.spec.tsx @@ -0,0 +1,56 @@ +import { renderHook, act } from '@testing-library/react'; +import { useState, type ReactNode } from 'react'; + +import EmojiPickerProvider from './EmojiPickerProvider'; +import { usePreviewEmoji } from '../../contexts/EmojiPickerContext'; + +jest.mock('@rocket.chat/fuselage-hooks', () => { + const originalModule = jest.requireActual('@rocket.chat/fuselage-hooks'); + + return { + ...originalModule, + useDebouncedState: (initialValue: T) => useState(initialValue), + useLocalStorage: (_: string, initialValue: T) => useState(initialValue), + useStableCallback: (callback: (...args: any[]) => unknown) => callback, + }; +}); + +jest.mock('../../../app/emoji/client', () => ({ + emoji: { packages: { base: { emojisByCategory: { recent: [] } } } }, + getFrequentEmoji: jest.fn(() => []), + createEmojiListByCategorySubscription: jest.fn(() => { + const snapshot = [[], {}]; + return [() => () => undefined, () => snapshot]; + }), +})); + +jest.mock('../../views/composer/EmojiPicker', () => () => null); +jest.mock('./useUpdateCustomEmoji', () => ({ useUpdateCustomEmoji: () => undefined })); + +const wrapper = ({ children }: { children: ReactNode }) => {children}; + +describe('EmojiPickerProvider', () => { + it('should keep the same preview reference when receiving the same emoji and name', () => { + const { result } = renderHook(() => usePreviewEmoji(), { wrapper }); + + act(() => { + result.current.handlePreview('image://smile', 'smile'); + }); + + const firstPreview = result.current.emojiToPreview; + expect(firstPreview).toEqual({ emoji: 'image://smile', name: 'smile' }); + + act(() => { + result.current.handlePreview('image://smile', 'smile'); + }); + + expect(result.current.emojiToPreview).toBe(firstPreview); + + act(() => { + result.current.handlePreview('image://rocket', 'rocket'); + }); + + expect(result.current.emojiToPreview).toEqual({ emoji: 'image://rocket', name: 'rocket' }); + expect(result.current.emojiToPreview).not.toBe(firstPreview); + }); +}); diff --git a/apps/meteor/client/providers/EmojiPickerProvider/EmojiPickerProvider.tsx b/apps/meteor/client/providers/EmojiPickerProvider/EmojiPickerProvider.tsx index 64538ef28df8c..469587722ac29 100644 --- a/apps/meteor/client/providers/EmojiPickerProvider/EmojiPickerProvider.tsx +++ b/apps/meteor/client/providers/EmojiPickerProvider/EmojiPickerProvider.tsx @@ -78,7 +78,16 @@ const EmojiPickerProvider = ({ children }: { children: ReactNode }) => { return setEmojiPicker( setEmojiPicker(null)} onPickEmoji={(emoji) => callback(emoji)} />); }, []); - const handlePreview = useCallback((emoji: string, name: string) => setEmojiToPreview({ emoji, name }), [setEmojiToPreview]); + const handlePreview = useCallback( + (emoji: string, name: string) => + setEmojiToPreview((preview) => { + if (preview?.emoji === emoji && preview?.name === name) { + return preview; + } + return { emoji, name }; + }), + [setEmojiToPreview], + ); const handleRemovePreview = useCallback(() => setEmojiToPreview(null), [setEmojiToPreview]); diff --git a/apps/meteor/client/views/composer/EmojiPicker/EmojiElement.tsx b/apps/meteor/client/views/composer/EmojiPicker/EmojiElement.tsx index e0ae6fad46113..94609766cc25a 100644 --- a/apps/meteor/client/views/composer/EmojiPicker/EmojiElement.tsx +++ b/apps/meteor/client/views/composer/EmojiPicker/EmojiElement.tsx @@ -34,7 +34,7 @@ const EmojiElement = ({ emoji, image, onClick, small = false, ...props }: EmojiE {...(small && { className: emojiSmallClass })} small={small} medium={!small} - onMouseOver={() => handlePreview(image, emoji)} + onMouseEnter={() => handlePreview(image, emoji)} onMouseLeave={handleRemovePreview} onClick={onClick} data-emoji={emoji} diff --git a/yarn.lock b/yarn.lock index 0a7e94a3958f6..7d05c79bccb08 100644 --- a/yarn.lock +++ b/yarn.lock @@ -10830,7 +10830,7 @@ __metadata: "@react-aria/toolbar": "*" "@rocket.chat/fuselage": "*" "@rocket.chat/icons": "*" - "@rocket.chat/ui-client": 32.0.0-rc.0 + "@rocket.chat/ui-client": 32.0.0 react: "*" react-dom: "*" languageName: unknown @@ -11084,7 +11084,7 @@ __metadata: peerDependencies: "@rocket.chat/layout": "*" "@rocket.chat/tools": 0.3.0 - "@rocket.chat/ui-contexts": 32.0.0-rc.0 + "@rocket.chat/ui-contexts": 32.0.0 "@tanstack/react-query": "*" react: "*" react-hook-form: "*"