@@ -138,3 +138,192 @@ describe('limel-menu', () => {
138138 } ) ;
139139 } ) ;
140140} ) ;
141+
142+ describe ( 'limel-menu keyboard navigation' , ( ) => {
143+ describe ( 'with nested items (sub-menus)' , ( ) => {
144+ let page : E2EPage ;
145+ let limelMenu : HTMLLimelMenuElement & E2EElement ;
146+
147+ beforeEach ( async ( ) => {
148+ page = await newE2EPage ( {
149+ html : `
150+ <limel-menu>
151+ <button slot="trigger">Open Menu</button>
152+ </limel-menu>
153+ ` ,
154+ } ) ;
155+ limelMenu = ( await page . find ( 'limel-menu' ) ) as any ;
156+ const items = [
157+ {
158+ text : 'Parent Item' ,
159+ items : [ { text : 'Child Item 1' } , { text : 'Child Item 2' } ] ,
160+ } ,
161+ { text : 'Another Item' } ,
162+ ] ;
163+ await limelMenu . setProperty ( 'items' , items ) ;
164+ await page . waitForChanges ( ) ;
165+ } ) ;
166+
167+ it ( 'right arrow on item with sub-menu emits navigateMenu' , async ( ) => {
168+ // Click the trigger button to open the menu (this sets up proper focus)
169+ const trigger = await page . find ( 'button' ) ;
170+ await trigger . click ( ) ;
171+ await page . waitForChanges ( ) ;
172+ await page . waitForTimeout ( 300 ) ;
173+
174+ const navigateMenuSpy = await page . spyOnEvent ( 'navigateMenu' ) ;
175+
176+ // Press right arrow to navigate into sub-menu (first item is focused by default)
177+ await page . keyboard . press ( 'ArrowRight' ) ;
178+ await page . waitForChanges ( ) ;
179+ await page . waitForTimeout ( 100 ) ;
180+
181+ expect ( navigateMenuSpy ) . toHaveReceivedEvent ( ) ;
182+ } ) ;
183+
184+ it ( 'left arrow in sub-menu goes back to parent' , async ( ) => {
185+ // Click to open the menu
186+ const trigger = await page . find ( 'button' ) ;
187+ await trigger . click ( ) ;
188+ await page . waitForChanges ( ) ;
189+ await page . waitForTimeout ( 300 ) ;
190+
191+ // Enter sub-menu with ArrowRight
192+ await page . keyboard . press ( 'ArrowRight' ) ;
193+ await page . waitForChanges ( ) ;
194+ await page . waitForTimeout ( 200 ) ;
195+
196+ // Verify we're in sub-menu
197+ let currentSubMenu = await limelMenu . getProperty ( 'currentSubMenu' ) ;
198+ expect ( currentSubMenu ) . not . toBeNull ( ) ;
199+
200+ // Press left to go back
201+ await page . keyboard . press ( 'ArrowLeft' ) ;
202+ await page . waitForChanges ( ) ;
203+ await page . waitForTimeout ( 100 ) ;
204+
205+ // Verify we're back at root
206+ currentSubMenu = await limelMenu . getProperty ( 'currentSubMenu' ) ;
207+ expect ( currentSubMenu ) . toBeNull ( ) ;
208+ } ) ;
209+
210+ it ( 'breadcrumbs visible when in sub-menu' , async ( ) => {
211+ // Click to open the menu
212+ const trigger = await page . find ( 'button' ) ;
213+ await trigger . click ( ) ;
214+ await page . waitForChanges ( ) ;
215+ await page . waitForTimeout ( 300 ) ;
216+
217+ // Navigate into sub-menu
218+ await page . keyboard . press ( 'ArrowRight' ) ;
219+ await page . waitForChanges ( ) ;
220+ await page . waitForTimeout ( 300 ) ;
221+
222+ // Verify we're actually in the sub-menu
223+ const currentSubMenu =
224+ await limelMenu . getProperty ( 'currentSubMenu' ) ;
225+ expect ( currentSubMenu ) . not . toBeNull ( ) ;
226+
227+ // Breadcrumbs are rendered in a portal (outside the menu's shadow DOM)
228+ // so we need to look for them in the document
229+ const breadcrumbsExists = await page . evaluate ( ( ) => {
230+ // First try looking in the portal container
231+ const portalContainer = document . querySelector (
232+ '.limel-portal--container'
233+ ) ;
234+ if (
235+ portalContainer ?. querySelector (
236+ 'limel-breadcrumbs, [class*="breadcrumb"]'
237+ )
238+ ) {
239+ return true ;
240+ }
241+
242+ // Check inside limel-menu-surface
243+ const menuSurface =
244+ document . querySelector ( 'limel-menu-surface' ) ;
245+ if ( menuSurface ) {
246+ const breadcrumbs =
247+ menuSurface . querySelector ( 'limel-breadcrumbs' ) ;
248+ if ( breadcrumbs ) {
249+ return true ;
250+ }
251+ }
252+
253+ // Also check if breadcrumbs exist anywhere in the document
254+ return document . querySelector ( 'limel-breadcrumbs' ) !== null ;
255+ } ) ;
256+ expect ( breadcrumbsExists ) . toBe ( true ) ;
257+ } ) ;
258+ } ) ;
259+
260+ describe ( 'without searcher (no input field)' , ( ) => {
261+ let page : E2EPage ;
262+ let limelMenu : HTMLLimelMenuElement & E2EElement ;
263+
264+ beforeEach ( async ( ) => {
265+ page = await newE2EPage ( {
266+ html : `
267+ <limel-menu>
268+ <button slot="trigger">Open Menu</button>
269+ </limel-menu>
270+ ` ,
271+ } ) ;
272+ limelMenu = ( await page . find ( 'limel-menu' ) ) as any ;
273+ const items = [
274+ { text : 'Item 1' } ,
275+ { text : 'Item 2' } ,
276+ { text : 'Item 3' } ,
277+ ] ;
278+ await limelMenu . setProperty ( 'items' , items ) ;
279+ await page . waitForChanges ( ) ;
280+ } ) ;
281+
282+ it ( 'no input field when no searcher is set' , async ( ) => {
283+ const trigger = await page . find ( 'button' ) ;
284+ await trigger . click ( ) ;
285+ await page . waitForChanges ( ) ;
286+ await page . waitForTimeout ( 200 ) ;
287+
288+ const inputExists = await page . evaluate ( ( ) => {
289+ const menu = document . querySelector ( 'limel-menu' ) ;
290+ const inputField =
291+ menu ?. shadowRoot ?. querySelector ( 'limel-input-field' ) ;
292+
293+ return inputField !== null ;
294+ } ) ;
295+ expect ( inputExists ) . toBe ( false ) ;
296+ } ) ;
297+
298+ it ( 'focus wraps within list items when no input field' , async ( ) => {
299+ const trigger = await page . find ( 'button' ) ;
300+ await trigger . click ( ) ;
301+ await page . waitForChanges ( ) ;
302+ await page . waitForTimeout ( 200 ) ;
303+
304+ // Navigate down from first item (Item 1 -> Item 2 -> Item 3)
305+ await page . keyboard . press ( 'ArrowDown' ) ;
306+ await page . keyboard . press ( 'ArrowDown' ) ;
307+ await page . waitForChanges ( ) ;
308+ await page . waitForTimeout ( 50 ) ;
309+
310+ // Press down once more - should wrap to first item (Item 3 -> Item 1)
311+ await page . keyboard . press ( 'ArrowDown' ) ;
312+ await page . waitForChanges ( ) ;
313+ await page . waitForTimeout ( 50 ) ;
314+
315+ const firstItemFocused = await page . evaluate ( ( ) => {
316+ const menu = document . querySelector ( 'limel-menu' ) ;
317+ const menuList =
318+ menu ?. shadowRoot ?. querySelector ( 'limel-menu-list' ) ;
319+ const listItems = menuList ?. shadowRoot ?. querySelectorAll (
320+ '.mdc-deprecated-list-item'
321+ ) ;
322+ const firstItem = listItems ?. [ 0 ] ;
323+
324+ return firstItem === menuList ?. shadowRoot ?. activeElement ;
325+ } ) ;
326+ expect ( firstItemFocused ) . toBe ( true ) ;
327+ } ) ;
328+ } ) ;
329+ } ) ;
0 commit comments