@@ -12,10 +12,23 @@ import userEvent from "@testing-library/user-event";
1212
1313import EmojiPicker from "../../../../../src/components/views/emojipicker/EmojiPicker" ;
1414import { stubClient } from "../../../../test-utils" ;
15+ import SettingsStore from "../../../../../src/settings/SettingsStore" ;
1516
1617describe ( "EmojiPicker" , function ( ) {
1718 stubClient ( ) ;
1819
20+ beforeEach ( ( ) => {
21+ // Clear recent emojis to prevent test pollution
22+ jest . spyOn ( SettingsStore , "getValue" ) . mockImplementation ( ( settingName ) => {
23+ if ( settingName === "recent_emoji" ) return [ ] as any ;
24+ return jest . requireActual ( "../../../../../src/settings/SettingsStore" ) . default . getValue ( settingName ) ;
25+ } ) ;
26+ } ) ;
27+
28+ afterEach ( ( ) => {
29+ jest . restoreAllMocks ( ) ;
30+ } ) ;
31+
1932 it ( "should not mangle default order after filtering" , async ( ) => {
2033 const ref = createRef < EmojiPicker > ( ) ;
2134 const { container } = render (
@@ -90,4 +103,64 @@ describe("EmojiPicker", function () {
90103 expect ( onChoose ) . toHaveBeenCalledWith ( "📫️" ) ;
91104 expect ( onFinished ) . toHaveBeenCalled ( ) ;
92105 } ) ;
106+
107+ it ( "should move actual focus when navigating between emojis after Tab" , async ( ) => {
108+ // mock offsetParent
109+ Object . defineProperty ( HTMLElement . prototype , "offsetParent" , {
110+ get ( ) {
111+ return this . parentNode ;
112+ } ,
113+ } ) ;
114+
115+ const onChoose = jest . fn ( ) ;
116+ const onFinished = jest . fn ( ) ;
117+ const { container } = render ( < EmojiPicker onChoose = { onChoose } onFinished = { onFinished } /> ) ;
118+
119+ const input = container . querySelector ( "input" ) ! ;
120+ expect ( input ) . toHaveFocus ( ) ;
121+
122+ // Wait for emojis to render
123+ await waitFor ( ( ) => {
124+ expect ( container . querySelector ( '[role="gridcell"]' ) ) . toBeInTheDocument ( ) ;
125+ } ) ;
126+
127+ function getEmoji ( ) : string {
128+ return document . activeElement ?. textContent || "" ;
129+ }
130+
131+ function getVirtuallyFocusedEmoji ( ) : string {
132+ const activeDescendant = input . getAttribute ( "aria-activedescendant" ) ;
133+ if ( ! activeDescendant ) return "" ;
134+ return container . querySelector ( "#" + activeDescendant ) ?. textContent || "" ;
135+ }
136+
137+ // Initially, arrow keys use virtual focus (aria-activedescendant)
138+ // The first emoji is virtually focused by default
139+ expect ( input ) . toHaveFocus ( ) ;
140+ expect ( getVirtuallyFocusedEmoji ( ) ) . toEqual ( "😀" ) ;
141+ expect ( getEmoji ( ) ) . toEqual ( "" ) ; // No actual emoji has focus
142+
143+ await userEvent . keyboard ( "[ArrowDown]" ) ;
144+ expect ( input ) . toHaveFocus ( ) ; // Input still has focus
145+ expect ( getVirtuallyFocusedEmoji ( ) ) . toEqual ( "🙂" ) ; // Virtual focus moved
146+ expect ( getEmoji ( ) ) . toEqual ( "" ) ; // No actual emoji has focus
147+
148+ // Tab to move actual focus to the emoji
149+ await userEvent . keyboard ( "[Tab]" ) ;
150+ expect ( input ) . not . toHaveFocus ( ) ;
151+ expect ( getEmoji ( ) ) . toEqual ( "🙂" ) ; // Now emoji has actual focus
152+
153+ // Arrow keys now move actual DOM focus between emojis
154+ await userEvent . keyboard ( "[ArrowDown]" ) ;
155+ expect ( getEmoji ( ) ) . toEqual ( "🤩" ) ; // Actual focus moved down one row
156+ expect ( input ) . not . toHaveFocus ( ) ;
157+
158+ await userEvent . keyboard ( "[ArrowUp]" ) ;
159+ expect ( getEmoji ( ) ) . toEqual ( "🙂" ) ; // Actual focus moved back up
160+ expect ( input ) . not . toHaveFocus ( ) ;
161+
162+ await userEvent . keyboard ( "[ArrowRight]" ) ;
163+ expect ( getEmoji ( ) ) . toEqual ( "🙃" ) ; // Actual focus moved right
164+ expect ( input ) . not . toHaveFocus ( ) ;
165+ } ) ;
93166} ) ;
0 commit comments