Repository: StatisticsFinland/PxVisualizer
Pull request #93: Hamburger menu accessibility
Author: login: sakari-malkki
Created: 2025-03-31T09:27:11Z | Merged: 2025-04-02T10:31:21Z

Aria labels and keyboard navigation fixes


Diff:
diff --git a/src/react/components/burgerMenu/__snapshots__/burgerMenu.test.tsx.snap b/src/react/components/burgerMenu/__snapshots__/burgerMenu.test.tsx.snap
index ed7ffdf..81bf273 100644
--- a/src/react/components/burgerMenu/__snapshots__/burgerMenu.test.tsx.snap
+++ b/src/react/components/burgerMenu/__snapshots__/burgerMenu.test.tsx.snap
@@ -30,7 +30,9 @@ exports[`burgerMenu, rendering tests Should render 1`] = `
     class="c0"
   >
     <button
+      aria-controls=":r0:-menu"
       aria-expanded="false"
+      aria-haspopup="menu"
       aria-label="Kuvion valikko"
       class="c1"
     >
diff --git a/src/react/components/burgerMenu/burgerMenu.test.tsx b/src/react/components/burgerMenu/burgerMenu.test.tsx
index cdf511b..5f867f8 100644
--- a/src/react/components/burgerMenu/burgerMenu.test.tsx
+++ b/src/react/components/burgerMenu/burgerMenu.test.tsx
@@ -29,14 +29,14 @@ const mockTableToggle = {
 }
 
 describe('burgerMenu, functional tests', () => {
-    it('Should open the menu with correct menu elements', async () => {
+    it('Should open the menu with correct elements', async () => {
         const view = convertPxGrafResponseToView(HORIZONTAL_BAR_CHART_ASCENDING, {});
         render(<BurgerMenu locale="fi" viewData={view} tableToggle={mockTableToggle} accessibilityMode={false} toggleAccessibilityMode={jest.fn()} />);
         act(() => {
             screen.getByRole('button').click();
         });
         await waitFor(() => {
-            expect(screen.getAllByRole('button').length).toEqual(5);
+            expect(screen.getAllByRole('menuitem').length).toEqual(4);
             expect(screen.getByText('Lataa taulukko (csv)')).toBeTruthy();
             expect(screen.getByText('Lataa taulukko (xlsx)')).toBeTruthy();
             expect(screen.getByText("N\u00E4yt\u00E4 taulukko")).toBeTruthy();
@@ -51,8 +51,7 @@ describe('burgerMenu, functional tests', () => {
             screen.getByRole('button').click();
         });
         await waitFor(() => {
-            expect(screen.getAllByRole('button').length).toEqual(7);
-            expect(screen.getAllByRole('link').length).toEqual(2);
+            expect(screen.getAllByRole('menuitem').length).toEqual(8);
             expect(screen.getByText('Lataa taulukko (csv)')).toBeTruthy();
             expect(screen.getByText('Lataa taulukko (xlsx)')).toBeTruthy();
             expect(screen.getByText('Foo')).toBeTruthy();
@@ -60,7 +59,6 @@ describe('burgerMenu, functional tests', () => {
             expect(screen.getByText('Baz')).toBeTruthy();
             expect(screen.getByText('Ulkoinen linkki')).toBeTruthy();
             expect(screen.getByText('Baz2')).toBeTruthy();
-            expect(screen.getAllByRole('link').length).toBe(2);
             expect(screen.getByText("N\u00E4yt\u00E4 taulukko")).toBeTruthy();
             expect(screen.getByText("N\u00E4yt\u00E4 kuviossa symbolit")).toBeTruthy();
         });
@@ -102,12 +100,10 @@ describe('burgerMenu, functional tests', () => {
         });
         await waitFor(() => {
             screen.getByText(btnText).click();
-            expect(mockFunction).toBeCalledTimes(1);
-        });
-        await waitFor(() => {
-            expect(screen.queryByText(btnText)).toBeNull();
-            expect(screen.queryByLabelText('Kuvion valikko')).not.toBeNull();
         });
+        expect(mockFunction).toBeCalledTimes(1);
+        expect(screen.queryByText(btnText)).toBeNull();
+        expect(screen.queryByRole('menu')).toBeNull();
     });
 
     it('Should invoke the custom function and close the menu when link is clicked', async () => {
@@ -120,12 +116,10 @@ describe('burgerMenu, functional tests', () => {
         });
         await waitFor(() => {
             screen.getByText(btnText).click();
-            expect(mockFunction).toBeCalledTimes(1);
-        });
-        await waitFor(() => {
-            expect(screen.queryByText(btnText)).toBeNull();
-            expect(screen.queryByLabelText('Kuvion valikko')).not.toBeNull();
         });
+        expect(mockFunction).toBeCalledTimes(1);
+        expect(screen.queryByText(btnText)).toBeNull();
+        expect(screen.queryByRole('menu')).toBeNull();
     });
 
     it('Should close the menu when ESC is pressed', async () => {
@@ -134,12 +128,22 @@ describe('burgerMenu, functional tests', () => {
         act(() => {
             screen.getByRole('button').click();
         });
-        await waitFor(() => {
-            window.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape' }));
+        act(() => {
+            fireEvent.keyDown(screen.getByRole('button'), { key: 'Escape' });
         });
-        await waitFor(() => {
-            expect(screen.queryByLabelText('Kuvion valikko')).not.toBeNull();
+        expect(screen.queryByRole('menu')).toBeNull();
+    });
+
+    it('Should close the menu when TAB is pressed', async () => {
+        const view = convertPxGrafResponseToView(HORIZONTAL_BAR_CHART_ASCENDING, {});
+        render(<BurgerMenu locale="fi" viewData={view} />);
+        act(() => {
+            screen.getByRole('button').click();
+        });
+        act(() => {
+            fireEvent.keyDown(screen.getByRole('button'), { key: 'Tab' });
         });
+        expect(screen.queryByRole('menu')).toBeNull();
     });
 
     it('Should calculate correct export dimensions when aspect ratio is too low', () => {
@@ -261,4 +265,75 @@ describe('burgerMenu, functional tests', () => {
             scale: 1
         }, {});
     });
+
+    it('should change focus and tabIndex when up and down arrows are pressed with the menu open', async () => {
+        const view = convertPxGrafResponseToView(HORIZONTAL_BAR_CHART_ASCENDING, {});
+        render(<BurgerMenu locale="fi" viewData={view} menuItemDefinitions={[{ text: 'Item1', onClick: jest.fn() }, { text: 'Item2', onClick: jest.fn() }]} />);
+        act(() => {
+            screen.getByRole('button').click();
+        });
+        await waitFor(() => {
+            expect(screen.getByText('Item1').closest('button')?.getAttribute('tabIndex')).toBe('0');
+            expect(screen.getByText('Item2').closest('button')?.getAttribute('tabIndex')).toBe('-1');
+        });
+        act(() => {
+            fireEvent.keyDown(screen.getByRole('button'), { key: 'ArrowDown' }); // window?
+        });
+        await waitFor(() => {
+            expect(screen.getByText('Item1').closest('button')?.getAttribute('tabIndex')).toBe('-1');
+            expect(screen.getByText('Item2').closest('button')?.getAttribute('tabIndex')).toBe('0');
+        });
+        act(() => {
+            fireEvent.keyDown(screen.getByRole('button'), { key: 'ArrowUp' }); // window?
+        });
+        await waitFor(() => {
+            expect(screen.getByText('Item1').closest('button')?.getAttribute('tabIndex')).toBe('0');
+            expect(screen.getByText('Item2').closest('button')?.getAttribute('tabIndex')).toBe('-1');
+        });
+    });
+
+    it('should cycle the focus of menu items back to start if down arrow is pressed on the last item', async () => {
+        const view = convertPxGrafResponseToView(HORIZONTAL_BAR_CHART_ASCENDING, {});
+        render(<BurgerMenu locale="fi" viewData={view}/>);
+        act(() => {
+            screen.getByRole('button').click();
+        });
+        await waitFor(() => {
+            expect(screen.getByText('Lataa taulukko (xlsx)').closest('button')?.getAttribute('tabIndex')).toBe('0');
+            expect(screen.getByText('Lataa taulukko (csv)').closest('button')?.getAttribute('tabIndex')).toBe('-1');
+        });
+        act(() => {
+            fireEvent.keyDown(screen.getByRole('button'), { key: 'ArrowDown' });
+        });
+        await waitFor(() => {
+            expect(screen.getByText('Lataa taulukko (xlsx)').closest('button')?.getAttribute('tabIndex')).toBe('-1');
+            expect(screen.getByText('Lataa taulukko (csv)').closest('button')?.getAttribute('tabIndex')).toBe('0');
+        });
+        act(() => {
+            fireEvent.keyDown(screen.getByRole('button'), { key: 'ArrowDown' });
+        });
+        await waitFor(() => {
+            expect(screen.getByText('Lataa taulukko (xlsx)').closest('button')?.getAttribute('tabIndex')).toBe('0');
+            expect(screen.getByText('Lataa taulukko (csv)').closest('button')?.getAttribute('tabIndex')).toBe('-1');
+        });
+    });
+
+    it('should cycle from first to last item if up arrow is pressed on the first item', async () => {
+        const view = convertPxGrafResponseToView(HORIZONTAL_BAR_CHART_ASCENDING, {});
+        render(<BurgerMenu locale="fi" viewData={view} />);
+        act(() => {
+            screen.getByRole('button').click();
+        });
+        await waitFor(() => {
+            expect(screen.getByText('Lataa taulukko (xlsx)').closest('button')?.getAttribute('tabIndex')).toBe('0');
+            expect(screen.getByText('Lataa taulukko (csv)').closest('button')?.getAttribute('tabIndex')).toBe('-1');
+        });
+        act(() => {
+            fireEvent.keyDown(screen.getByRole('button'), { key: 'ArrowUp' });
+        });
+        await waitFor(() => {
+            expect(screen.getByText('Lataa taulukko (xlsx)').closest('button')?.getAttribute('tabIndex')).toBe('-1');
+            expect(screen.getByText('Lataa taulukko (csv)').closest('button')?.getAttribute('tabIndex')).toBe('0');
+        });
+    });
 });
\ No newline at end of file
diff --git a/src/react/components/burgerMenu/burgerMenu.tsx b/src/react/components/burgerMenu/burgerMenu.tsx
index cefe618..d6f9725 100644
--- a/src/react/components/burgerMenu/burgerMenu.tsx
+++ b/src/react/components/burgerMenu/burgerMenu.tsx
@@ -1,4 +1,4 @@
-import React, { useEffect } from "react";
+import React, { useEffect, useId } from "react";
 import styled from "styled-components";
 import { MenuItem } from "./menuItem/menuItem";
 import HighchartsReact, { HighchartsReactRefObject } from "highcharts-react-official";
@@ -121,32 +121,185 @@ export function calculateExportDimensions(chartRef: HighchartsReactRefObject): {
     return { finalWidth, finalHeight };
 }
 
+const getTabIndex = (index: number, selectedIndex: number) => {
+    return selectedIndex != -1 && selectedIndex === index ? 0 : -1;
+};
 
 export const BurgerMenu: React.FC<IBurgerMenuProps> = ({ viewData, currentChartRef, tableToggle, menuItemDefinitions, locale, menuIconInheritColor = false, toggleAccessibilityMode, accessibilityMode }) => {
     const [isOpen, setIsOpen] = React.useState(false);
+    const [selectedIndex, setSelectedIndex] = React.useState(-1);
+    const idPrefix: string = useId();
 
+    const menuItems = React.useRef(new Map<number, HTMLButtonElement | HTMLAnchorElement | null>());
     const menuRef = React.useRef<any>(null);
     const closeMenu = (e: Event) => {
-        if(menuRef.current && isOpen && !menuRef.current.contains(e.target)) {
-            setIsOpen(false);
-            buttonRef.current?.focus();
-          }
+        if (menuRef.current && isOpen && !menuRef.current.contains(e.target)) {
+            toggleMenuOpen(false);
+        }
     }
 
+    React.useEffect(() => {
+        setFocusToMenuItem();
+    }, [selectedIndex]);
+
     document.addEventListener('mousedown', closeMenu);
     const exportCSV = viewToDownloadCSVOption(viewData, locale);
     const exportXLSX = viewToDownloadXLSOption(viewData, locale);
     const customMenuItemArray = menuItemDefinitions?.map((menuItemDefinition, index) => {
         if ('onClick' in menuItemDefinition) {
-            return <MenuItem isFirst={index === 0} bottomSeparator={index + 1 === menuItemDefinitions.length} locale={locale} prefixIcon={menuItemDefinition.prefixIcon} suffixIcon={menuItemDefinition.suffixIcon} key={`customfmenuitem-${index}`} text={menuItemDefinition.text} onClick={() => handleMenuItemClick(menuItemDefinition.onClick)} />; // NOSONAR
+            return <MenuItem
+                tabIndex={getTabIndex(index, selectedIndex)}
+                isFirst={index === 0}
+                index={index}
+                bottomSeparator={index + 1 === menuItemDefinitions.length}
+                locale={locale}
+                prefixIcon={menuItemDefinition.prefixIcon}
+                suffixIcon={menuItemDefinition.suffixIcon}
+                key={`customfmenuitem-${menuItemDefinition.text}`}
+                text={menuItemDefinition.text}
+                onClick={() => handleMenuItemClick(menuItemDefinition.onClick)}
+                ref={(el) => menuItems.current.set(index, el)}
+                idPrefix={idPrefix}
+            />; // NOSONAR
         }
 
         if ('url' in menuItemDefinition) {
-            return <MenuItem isFirst={index === 0} bottomSeparator={index + 1 === menuItemDefinitions.length} locale={locale} prefixIcon={menuItemDefinition.prefixIcon} suffixIcon={menuItemDefinition.suffixIcon} isExternal={menuItemDefinition.isExternal} key={`customlmenuitem-${index}`} text={`${menuItemDefinition.text}`} url={menuItemDefinition.url} openNewTab={menuItemDefinition.openNewTab} onClick={() => handleMenuItemClick()} />; // NOSONAR
+            return <MenuItem
+                tabIndex={getTabIndex(index, selectedIndex)}
+                isFirst={index === 0}
+                index={index}
+                bottomSeparator={index + 1 === menuItemDefinitions.length}
+                locale={locale}
+                prefixIcon={menuItemDefinition.prefixIcon}
+                suffixIcon={menuItemDefinition.suffixIcon}
+                isExternal={menuItemDefinition.isExternal}
+                key={`customlmenuitem-${menuItemDefinition.text}`}
+                text={`${menuItemDefinition.text}`}
+                url={menuItemDefinition.url}
+                openNewTab={menuItemDefinition.openNewTab}
+                onClick={() => handleMenuItemClick()}
+                ref={(el) => menuItems.current.set(index, el)}
+                idPrefix={idPrefix}
+            />; // NOSONAR
         }
     });
 
+    const defaultMenuItemsArray = () => {
+        const items = [];
+        let itemIndex = menuItemDefinitions ? menuItemDefinitions.length : 0;
+
+        const createRefCallback = (index: number) => (el: HTMLButtonElement | HTMLAnchorElement | null) => {
+            menuItems.current.set(index, el);
+        };
+
+        items.push(<MenuItem
+            index={itemIndex}
+            isFirst={!menuItemDefinitions}
+            locale={locale}
+            prefixIcon={'Download'}
+            text={exportXLSX.text}
+            onClick={() => handleMenuItemClick(exportXLSX.onClick)}
+            tabIndex={getTabIndex(itemIndex, selectedIndex)}
+            ref={createRefCallback(itemIndex)}
+            key={`xlsx-menu-item`}
+            idPrefix={idPrefix}
+        />);
+
+        itemIndex++;
+        items.push(<MenuItem
+            index={itemIndex}
+            isLast={!currentChartRef}
+            bottomSeparator={!!currentChartRef}
+            locale={locale}
+            prefixIcon={'Download'}
+            text={exportCSV.text}
+            onClick={() => handleMenuItemClick(exportCSV.onClick)}
+            tabIndex={getTabIndex(itemIndex, selectedIndex)}
+            ref={createRefCallback(itemIndex)}
+            key={`csv-menu-item`}
+            idPrefix={idPrefix}
+        />);
+
+        if (currentChartRef) {
+            itemIndex++;
+            items.push(<MenuItem
+                index={itemIndex}
+                locale={locale}
+                prefixIcon={'Download'}
+                text={Translations.downloadSVG[locale]}
+                onClick={() => handleMenuItemClick(() =>
+                    currentChartRef.chart.exportChartLocal({
+                        filename: `${generateFilename(viewData.tableReferenceName)}`,
+                        type: "image/svg+xml",
+                        sourceWidth: calculateExportDimensions(currentChartRef).finalWidth,
+                        sourceHeight: calculateExportDimensions(currentChartRef).finalHeight,
+                        scale: 1
+                    }, {}))
+                }
+                tabIndex={getTabIndex(itemIndex, selectedIndex)}
+                ref={createRefCallback(itemIndex)}
+                key={`svg-menu-item`}
+                idPrefix={idPrefix}
+            />)
+
+            itemIndex++;
+            items.push(<MenuItem
+                index={itemIndex}
+                isLast={!tableToggle}
+                bottomSeparator={!!tableToggle}
+                locale={locale}
+                prefixIcon={'Download'}
+                text={Translations.downloadPNG[locale]}
+                onClick={() => handleMenuItemClick(() =>
+                    currentChartRef.chart.exportChartLocal({
+                        filename: `${generateFilename(viewData.tableReferenceName)}`,
+                        sourceWidth: calculateExportDimensions(currentChartRef).finalWidth,
+                        sourceHeight: calculateExportDimensions(currentChartRef).finalHeight,
+                        scale: 1
+                    }, {}))
+                }
+                tabIndex={getTabIndex(itemIndex, selectedIndex)}
+                ref={createRefCallback(itemIndex)}
+                key={`png-menu-item`}
+                idPrefix={idPrefix}
+            />)
+        }
+
+        if (showAccessibilityModeToggle) {
+            itemIndex++;
+            items.push(<MenuItem
+                index={itemIndex}
+                isFirst={false}
+                locale={locale}
+                text={accessibilityMode ? Translations.toggleAccessibilityModeOff[locale] : Translations.toggleAccessibilityModeOn[locale]}
+                onClick={() => handleMenuItemClick(toggleAccessibilityMode)}
+                tabIndex={getTabIndex(itemIndex, selectedIndex)}
+                ref={createRefCallback(itemIndex)}
+                key={`accessibility-mode-toggle`}
+                idPrefix={idPrefix}
+            />)
+        }
+
+        if (tableToggle) {
+            itemIndex++;
+            items.push(<MenuItem
+                index={itemIndex}
+                isLast={true}
+                locale={locale}
+                text={tableToggle.tableMode ? Translations.toggleTableModeOffText[locale] : Translations.toggleTableModeOnText[locale]}
+                onClick={() => handleMenuItemClick(tableToggle.toggleHandler)}
+                tabIndex={getTabIndex(itemIndex, selectedIndex)}
+                ref={createRefCallback(itemIndex)}
+                key={`table-toggle`}
+                idPrefix={idPrefix}
+            />)
+        }
+
+        return items;
+    };
+
     useEffect(() => {
+        setSelectedIndex(isOpen ? 0 : -1);
         document.addEventListener('mousedown', closeMenu);
         document.addEventListener('keydown', handleKeyDown);
         return () => {
@@ -155,20 +308,53 @@ export const BurgerMenu: React.FC<IBurgerMenuProps> = ({ viewData, currentChartR
         }
     }, [isOpen]);
 
+    const getMenuItemMaxIndex = (): number => {
+        const customMenuItemsCount: number = menuItemDefinitions ? menuItemDefinitions.length : 0;
+        return customMenuItemsCount + defaultMenuItemsArray().length - 1;
+    }
+
     const handleKeyDown = (e: KeyboardEvent) => {
-        if (e.key === 'Escape') {
-            setIsOpen(false);
-            buttonRef.current?.focus();
+        if (e.key === 'Escape' ||
+        (e.key === 'Tab' && isOpen)) {
+            toggleMenuOpen(false);
+        }
+        if (e.key === 'ArrowDown' && isOpen) {
+            e.preventDefault();
+            setSelectedIndex((prevIndex) => {
+                const nextIndex = prevIndex + 1;
+                const maxIndex = getMenuItemMaxIndex();
+                return nextIndex > maxIndex ? 0 : nextIndex;
+            });
+        }
+        if (e.key === 'ArrowUp' && isOpen) {
+            e.preventDefault();
+            setSelectedIndex((prevIndex) => {
+                const nextIndex = prevIndex - 1;
+                const maxIndex = getMenuItemMaxIndex();
+                return nextIndex < 0 ? maxIndex : nextIndex;
+            });
         }
     }
 
     const buttonRef = React.useRef<HTMLButtonElement>(null);
     const handleMenuItemClick = (onClick?: () => void) => {
-        setIsOpen(false);
-        buttonRef.current?.focus();
+        toggleMenuOpen(false);
         if (onClick) onClick();
     }
 
+    const toggleMenuOpen = (open: boolean) => {
+        setIsOpen(open);
+    }
+
+    const setFocusToMenuItem = () => {
+        if (selectedIndex === -1) {
+            return;
+        }
+        if (menuItems.current.has(selectedIndex)) {
+            menuItems.current.get(selectedIndex)?.focus();
+        }
+    }
+
     const showAccessibilityModeToggle: boolean =
         !!tableToggle &&
         !tableToggle.tableMode &&
@@ -176,59 +362,18 @@ export const BurgerMenu: React.FC<IBurgerMenuProps> = ({ viewData, currentChartR
 
     return (
         <BurgerWrapper ref={menuRef}>
-            <Hamburger ref={buttonRef} aria-label={`${Translations.chartMenuLabel[locale]}`} aria-expanded={isOpen} onClick={() => {setIsOpen(!isOpen)}}>
+            <Hamburger ref={buttonRef} aria-label={`${Translations.chartMenuLabel[locale]}`} aria-expanded={isOpen} onClick={() => { toggleMenuOpen(!isOpen) }} aria-haspopup="menu" aria-controls={`${idPrefix}-menu`}>
                 <Icon inheritColor={menuIconInheritColor} icon={isOpen ? 'Times' : 'Bars'} />
             </Hamburger>
             <MenuAnchor>
                 {
                     isOpen &&
                     <MenuWrapper>
-                        <List>
-                            {menuItemDefinitions && customMenuItemArray}
-                            <MenuItem isFirst={!menuItemDefinitions} locale={locale} prefixIcon={'Download'} text={exportXLSX.text} onClick={() => handleMenuItemClick(exportXLSX.onClick) } />
-                            <MenuItem isLast={!currentChartRef} bottomSeparator={!!currentChartRef} locale={locale} prefixIcon={'Download'} text={exportCSV.text} onClick={() => handleMenuItemClick(exportCSV.onClick) } />
-                            {
-                                    currentChartRef &&
-                                    <>
-                                        <MenuItem
-                                            locale={locale}
-                                            prefixIcon={'Download'}
-                                            text={Translations.downloadSVG[locale]}
-                                            onClick={() => handleMenuItemClick(() =>
-                                                currentChartRef.chart.exportChartLocal({
-                                                    filename: `${generateFilename(viewData.tableReferenceName)}`,
-                                                    type: "image/svg+xml",
-                                                    sourceWidth: calculateExportDimensions(currentChartRef).finalWidth,
-                                                    sourceHeight: calculateExportDimensions(currentChartRef).finalHeight,
-                                                    scale: 1
-                                                }, {}))
-                                            } />
-                                        <MenuItem
-                                            isLast={!tableToggle}
-                                            bottomSeparator={!!tableToggle}
-                                            locale={locale}
-                                            prefixIcon={'Download'}
-                                            text={Translations.downloadPNG[locale]}
-                                            onClick={() => handleMenuItemClick(() =>
-                                                currentChartRef.chart.exportChartLocal({
-                                                    filename: `${generateFilename(viewData.tableReferenceName)}`,
-                                                    sourceWidth: calculateExportDimensions(currentChartRef).finalWidth,
-                                                    sourceHeight: calculateExportDimensions(currentChartRef).finalHeight,
-                                                    scale: 1
-                                                }, {}))
-                                            } />
-                                    </>
-                                }
-                                {
-                                    showAccessibilityModeToggle &&
-                                    <MenuItem isFirst={false} locale={locale} text={accessibilityMode ? Translations.toggleAccessibilityModeOff[locale] : Translations.toggleAccessibilityModeOn[locale]} onClick={() => handleMenuItemClick(toggleAccessibilityMode)} />
-                                }
-                                {
-                                    tableToggle &&
-                                    <MenuItem isLast={true} locale={locale} text={tableToggle.tableMode ? Translations.toggleTableModeOffText[locale] : Translations.toggleTableModeOnText[locale]} onClick={() => handleMenuItemClick(tableToggle.toggleHandler)} />
-                                }
+                            <List id={`${idPrefix}-menu`} role="menu" aria-label={`${Translations.chartMenuLabel[locale]}`} aria-orientation="vertical" aria-activedescendant={`${idPrefix}-menuitem-${selectedIndex}`} tabIndex={isOpen ? 0 : -1}>
+                                {menuItemDefinitions && customMenuItemArray}
+                                {defaultMenuItemsArray()}
                             </List>
-                    </MenuWrapper>
+                        </MenuWrapper>
                 }
             </MenuAnchor>
         </BurgerWrapper>
diff --git a/src/react/components/burgerMenu/menuItem/__snapshots__/menuItem.test.tsx.snap b/src/react/components/burgerMenu/menuItem/__snapshots__/menuItem.test.tsx.snap
index 73e7ea4..0a752ff 100644
--- a/src/react/components/burgerMenu/menuItem/__snapshots__/menuItem.test.tsx.snap
+++ b/src/react/components/burgerMenu/menuItem/__snapshots__/menuItem.test.tsx.snap
@@ -68,6 +68,9 @@ exports[`MenuItem, rendering tests should render 1`] = `
   >
     <button
       class="c1"
+      id="foo-menuitem--1"
+      role="menuitem"
+      tabindex="-1"
     >
       <div
         class="c2"
@@ -162,6 +165,9 @@ exports[`MenuItem, rendering tests should render with correct custom suffix icon
   >
     <button
       class="c1"
+      id="foo-menuitem--1"
+      role="menuitem"
+      tabindex="-1"
     >
       <div
         class="c2"
@@ -276,6 +282,10 @@ exports[`MenuItem, rendering tests should render with correct external icon 1`]
     <a
       class="c1"
       href="foobar"
+      id="foo-menuitem--1"
+      role="menuitem"
+      tabindex="-1"
+      title="[object Object]"
     >
       <div
         class="c2"
@@ -393,6 +403,9 @@ exports[`MenuItem, rendering tests should render with correct prefix icon 1`] =
   >
     <button
       class="c1"
+      id="foo-menuitem--1"
+      role="menuitem"
+      tabindex="-1"
     >
       <div
         class="c2"
@@ -505,6 +518,9 @@ exports[`MenuItem, rendering tests should render with correct suffix icon 1`] =
   >
     <button
       class="c1"
+      id="foo-menuitem--1"
+      role="menuitem"
+      tabindex="-1"
     >
       <div
         class="c2"
diff --git a/src/react/components/burgerMenu/menuItem/menuItem.test.tsx b/src/react/components/burgerMenu/menuItem/menuItem.test.tsx
index a8247b7..7060f4b 100644
--- a/src/react/components/burgerMenu/menuItem/menuItem.test.tsx
+++ b/src/react/components/burgerMenu/menuItem/menuItem.test.tsx
@@ -11,23 +11,23 @@ const mockImg = (
 
 describe('MenuItem, rendering tests', () => {
     it('should render', () => {
-        const {asFragment} = render(<MenuItem locale={'fi'} text={'NAPPI'} onClick={mockFunction} />);
+        const { asFragment } = render(<MenuItem locale={'fi'} text={'NAPPI'} onClick={mockFunction} idPrefix='foo' />);
         expect(asFragment()).toMatchSnapshot();
     });
     it('should render with correct prefix icon', () => {
-        const {asFragment} = render(<MenuItem prefixIcon={'Download'} locale={'fi'} text={'NAPPI'} onClick={mockFunction} />);
+        const { asFragment } = render(<MenuItem prefixIcon={'Download'} locale={'fi'} text={'NAPPI'} onClick={mockFunction} idPrefix='foo' />);
         expect(asFragment()).toMatchSnapshot();
     });
     it('should render with correct suffix icon', () => {
-        const {asFragment} = render(<MenuItem suffixIcon={'Download'} locale={'fi'} text={'NAPPI'} onClick={mockFunction} />);
+        const { asFragment } = render(<MenuItem suffixIcon={'Download'} locale={'fi'} text={'NAPPI'} onClick={mockFunction} idPrefix='foo' />);
         expect(asFragment()).toMatchSnapshot();
     });
     it('should render with correct external icon', () => {
-        const {asFragment} = render(<MenuItem isExternal={true} locale={'fi'} text={'NAPPI'} url={'foobar'} />);
+        const { asFragment } = render(<MenuItem isExternal={true} locale={'fi'} text={'NAPPI'} url={'foobar'} idPrefix='foo' />);
         expect(asFragment()).toMatchSnapshot();
     });
     it('should render with correct custom suffix icon', () => {
-        const {asFragment} = render(<MenuItem suffixIcon={mockImg} locale={'fi'} text={'NAPPI'} onClick={mockFunction} />);
+        const { asFragment } = render(<MenuItem suffixIcon={mockImg} locale={'fi'} text={'NAPPI'} onClick={mockFunction} idPrefix='foo' />);
         expect(asFragment()).toMatchSnapshot();
     });
 });
@@ -39,7 +39,7 @@ describe('MenuItem, functional tests', () => {
 
     it('should call onclick function when clicked', () => {
         const btnText = 'PRESSME';
-        render(<MenuItem locale={'fi'} text={btnText} onClick={mockFunction} />);
+        render(<MenuItem locale={'fi'} text={btnText} onClick={mockFunction} idPrefix='foo' />);
         screen.getByText(btnText).click();
         expect(mockFunction).toHaveBeenCalledTimes(1);
         screen.getByText(btnText).click();
@@ -48,15 +48,15 @@ describe('MenuItem, functional tests', () => {
 
     it('should render a link that calls onclick function when clicked', () => {
         const btnText = 'PRESSME';
-        render(<MenuItem locale={'fi'} text={btnText} url={'foobar.fi'} openNewTab={true} onClick={mockFunction} />);
-        expect(screen.getAllByRole('link').length).toBe(1);
+        render(<MenuItem locale={'fi'} text={btnText} url={'foobar.fi'} openNewTab={true} onClick={mockFunction} idPrefix='foo' />);
+        expect(screen.getAllByRole('menuitem').length).toBe(1);
         screen.getByText(btnText).click();
         expect(mockFunction).toHaveBeenCalledTimes(1);
     });
 
     it('should call onclick function when spacebar is pressed', () => {
         const btnText = 'PRESSME';
-        render(<MenuItem locale={'fi'} text={btnText} url={'foobar.fi'} openNewTab={true} onClick={mockFunction} />);
+        render(<MenuItem locale={'fi'} text={btnText} url={'foobar.fi'} openNewTab={true} onClick={mockFunction} idPrefix='foo' />);
         screen.getByText(btnText).focus();
         act(() => {
             fireEvent.keyDown(screen.getByText(btnText), { key: ' ' });
@@ -66,7 +66,7 @@ describe('MenuItem, functional tests', () => {
 
     it('should render nothing if functionality is not declared', () => {
         const btnText = 'PRESSME';
-        render(<MenuItem locale={'fi'} text={btnText} />);
+        render(<MenuItem locale={'fi'} text={btnText} idPrefix='foo' />);
         expect(screen.queryByText(btnText)).toBeNull();
     });
 });
\ No newline at end of file
diff --git a/src/react/components/burgerMenu/menuItem/menuItem.tsx b/src/react/components/burgerMenu/menuItem/menuItem.tsx
index df5c1e8..86fb057 100644
--- a/src/react/components/burgerMenu/menuItem/menuItem.tsx
+++ b/src/react/components/burgerMenu/menuItem/menuItem.tsx
@@ -1,4 +1,4 @@
-import React from "react";
+import React, { forwardRef } from "react";
 import styled from "styled-components";
 import { TIcon, allIcons } from "../../../../core/types/icon";
 import { Icon } from "../../icon/icon";
@@ -14,8 +14,7 @@ interface ITextWrapperProps {
     $hasPrefixIcon?: boolean;
     $hasSuffixIcon?: boolean;
 }
-
-const Button = styled.button`
+const Button = styled(forwardRef<HTMLButtonElement, React.ComponentPropsWithoutRef<'button'>>((props, ref) => (<button ref={ref} {...props} />)))`
     background-color: transparent;
     text-align: left;
     border: none;
@@ -33,7 +32,7 @@ const Button = styled.button`
     }
 `;
 
-const StyledLink = styled.a`
+const StyledLink = styled(forwardRef<HTMLAnchorElement, React.ComponentPropsWithoutRef<'a'>>((props, ref) => (<a ref={ref} {...props} title={props.children ? props.children.toString() : ''} />)))`
     text-decoration: none;
     display: flex;
     align-items: center;
@@ -107,9 +106,12 @@ export interface IMenuItemProps {
     bottomSeparator?: boolean;
     isFirst?: boolean;
     isLast?: boolean;
+    tabIndex?: number;
+    index?: number;
+    idPrefix: string;
 }
 
-export const MenuItem: React.FC<IMenuItemProps> = ({text, onClick, url, openNewTab, isExternal, prefixIcon, suffixIcon, locale, bottomSeparator = false, isFirst = false, isLast = false}) => {
+export const MenuItem = forwardRef<HTMLAnchorElement | HTMLButtonElement, IMenuItemProps>(({ text, onClick, url, openNewTab, isExternal, prefixIcon, suffixIcon, locale, idPrefix, bottomSeparator = false, isFirst = false, isLast = false, tabIndex = -1, index = -1 }, ref) => {
 
     let prefixIconContent: React.ReactNode;
     if (prefixIcon) {
@@ -152,7 +154,7 @@ export const MenuItem: React.FC<IMenuItemProps> = ({text, onClick, url, openNewT
     if (url) {
         return (
             <ListItem $isFirst={isFirst} $isLast={isLast} $separator={bottomSeparator}>
-                <StyledLink href={url} target={openNewTab ? '_blank' : undefined} onKeyDown={handleKeyDown} onClick={handleClick}>
+                <StyledLink role="menuitem" id={`${idPrefix}-menuitem-${index}`} ref={ref as React.Ref<HTMLAnchorElement>} href={url} target={openNewTab ? '_blank' : undefined} onKeyDown={handleKeyDown} onClick={handleClick} tabIndex={tabIndex}>
                     {content}
                 </StyledLink>
             </ListItem>
@@ -160,7 +162,11 @@ export const MenuItem: React.FC<IMenuItemProps> = ({text, onClick, url, openNewT
     }
 
     if (onClick) {
-        return (<ListItem $isFirst={isFirst} $isLast={isLast} $separator={bottomSeparator}><Button onClick={onClick}>{content}</Button></ListItem>)
+        return (<ListItem $isFirst={isFirst} $isLast={isLast} $separator={bottomSeparator}>
+            <Button role="menuitem" id={`${idPrefix}-menuitem-${index}`} ref={ref as React.Ref<HTMLButtonElement>} onClick={onClick} tabIndex={tabIndex}>
+                {content}
+                </Button>
+            </ListItem>)
     }
     return (<></>);
-};
\ No newline at end of file
+});
\ No newline at end of file
diff --git a/src/react/components/chart/__snapshots__/chart.test.tsx.snap b/src/react/components/chart/__snapshots__/chart.test.tsx.snap
index f3efe71..a03fba2 100644
--- a/src/react/components/chart/__snapshots__/chart.test.tsx.snap
+++ b/src/react/components/chart/__snapshots__/chart.test.tsx.snap
@@ -12,7 +12,9 @@ exports[`Rendering test renders chart data correctly 1`] = `
         class="sc-kAyceB geaItU"
       >
         <button
+          aria-controls=":r0:-menu"
           aria-expanded="false"
+          aria-haspopup="menu"
           aria-label="Kuvion valikko"
           class="sc-imWYAI fEthAU"
         >
@@ -143,7 +145,9 @@ exports[`Rendering test renders table data correctly 1`] = `
         class="sc-kAyceB geaItU"
       >
         <button
+          aria-controls=":r1:-menu"
           aria-expanded="false"
+          aria-haspopup="menu"
           aria-label="Kuvion valikko"
           class="sc-imWYAI fEthAU"
         >
@@ -816,7 +820,9 @@ exports[`Rendering test renders table data correctly when given footnote 1`] = `
         class="sc-kAyceB geaItU"
       >
         <button
+          aria-controls=":r5:-menu"
           aria-expanded="false"
+          aria-haspopup="menu"
           aria-label="Kuvion valikko"
           class="sc-imWYAI fEthAU"
         >
@@ -1492,7 +1498,9 @@ exports[`Rendering test renders table data correctly when sources are on 1`] = `
         class="sc-kAyceB geaItU"
       >
         <button
+          aria-controls=":r4:-menu"
           aria-expanded="false"
+          aria-haspopup="menu"
           aria-label="Kuvion valikko"
           class="sc-imWYAI fEthAU"
         >
@@ -2168,7 +2176,9 @@ exports[`Rendering test renders table data correctly when titles are forced on 1
         class="sc-kAyceB geaItU"
       >
         <button
+          aria-controls=":r2:-menu"
           aria-expanded="false"
+          aria-haspopup="menu"
           aria-label="Kuvion valikko"
           class="sc-imWYAI fEthAU"
         >
@@ -2846,7 +2856,9 @@ exports[`Rendering test renders table data correctly when units are on 1`] = `
         class="sc-kAyceB geaItU"
       >
         <button
+          aria-controls=":r3:-menu"
           aria-expanded="false"
+          aria-haspopup="menu"
           aria-label="Kuvion valikko"
           class="sc-imWYAI fEthAU"
         >


Reviews:
user: login: Z-kke
state: APPROVED
author_association: NONE
submitted_at: 2025-04-01T10:03:57Z
user: login: JLaiTre
state: APPROVED
author_association: NONE
submitted_at: 2025-04-02T10:27:49Z
user: login: sakari-malkki
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-02T10:29:09Z
user: login: sakari-malkki
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-02T10:29:20Z
user: login: lehtu
state: APPROVED
author_association: COLLABORATOR
submitted_at: 2025-04-02T10:31:20Z

Review comments:
diff_hunk: @@ -155,80 +299,72 @@ export const BurgerMenu: React.FC<IBurgerMenuProps> = ({ viewData, currentChartR
         }
     }, [isOpen]);
 
+    const getMenuItemMaxIndex = (): number => {
+        const customMenuItemsCount: number = menuItemDefinitions ? menuItemDefinitions.length : 0;
+        return customMenuItemsCount + defaultMenuItemsArray().length - 1;
+    }
+
     const handleKeyDown = (e: KeyboardEvent) => {
-        if (e.key === 'Escape') {
-            setIsOpen(false);
-            buttonRef.current?.focus();
+        if (e.key === 'Escape' ||
+        (e.key === 'Tab' && isOpen)) {
+            toggleMenuOpen(false);
+        }
+        if (e.key === 'ArrowDown' && isOpen) {
+            e.preventDefault();
+            setSelectedIndex((prevIndex) => {
+                const nextIndex = prevIndex + 1;
+                const maxIndex = getMenuItemMaxIndex();
+                return nextIndex > maxIndex ? 0 : nextIndex;
+            });
+        }
+        if (e.key === 'ArrowUp' && isOpen) {
+            e.preventDefault();
+            setSelectedIndex((prevIndex) => {
+                const nextIndex = prevIndex - 1;
+                const maxIndex = getMenuItemMaxIndex();
+                return nextIndex < 0 ? maxIndex : nextIndex;
+            });
         }
     }
 
     const buttonRef = React.useRef<HTMLButtonElement>(null);
     const handleMenuItemClick = (onClick?: () => void) => {
-        setIsOpen(false);
-        buttonRef.current?.focus();
+        toggleMenuOpen(false);
         if (onClick) onClick();
     }
 
+    const toggleMenuOpen = (open: boolean) => {
+        setIsOpen(open);
+    }
+
+    const setFocusToMenuItem = () => {
+        if (selectedIndex === -1) {
+            return;
+        }
+        if (menuItems.current.has(selectedIndex)) {
+            menuItems.current.get(selectedIndex)?.focus();
+        }
+    }
+
     const showAccessibilityModeToggle: boolean =
         !!tableToggle &&
         !tableToggle.tableMode &&
         viewData.visualizationSettings.visualizationType != EVisualizationType.ScatterPlot;
 
     return (
         <BurgerWrapper ref={menuRef}>
-            <Hamburger ref={buttonRef} aria-label={`${Translations.chartMenuLabel[locale]}`} aria-expanded={isOpen} onClick={() => {setIsOpen(!isOpen)}}>
+            <Hamburger ref={buttonRef} aria-label={`${Translations.chartMenuLabel[locale]}`} aria-expanded={isOpen} onClick={() => { toggleMenuOpen(!isOpen) }} aria-haspopup="menu" aria-controls="menu">
                 <Icon inheritColor={menuIconInheritColor} icon={isOpen ? 'Times' : 'Bars'} />
             </Hamburger>
             <MenuAnchor>
                 {
                     isOpen &&
-                    <MenuWrapper>
-                        <List>
-                            {menuItemDefinitions && customMenuItemArray}
-                            <MenuItem isFirst={!menuItemDefinitions} locale={locale} prefixIcon={'Download'} text={exportXLSX.text} onClick={() => handleMenuItemClick(exportXLSX.onClick) } />
-                            <MenuItem isLast={!currentChartRef} bottomSeparator={!!currentChartRef} locale={locale} prefixIcon={'Download'} text={exportCSV.text} onClick={() => handleMenuItemClick(exportCSV.onClick) } />
-                            {
-                                    currentChartRef &&
-                                    <>
-                                        <MenuItem
-                                            locale={locale}
-                                            prefixIcon={'Download'}
-                                            text={Translations.downloadSVG[locale]}
-                                            onClick={() => handleMenuItemClick(() =>
-                                                currentChartRef.chart.exportChartLocal({
-                                                    filename: `${generateFilename(viewData.tableReferenceName)}`,
-                                                    type: "image/svg+xml",
-                                                    sourceWidth: calculateExportDimensions(currentChartRef).finalWidth,
-                                                    sourceHeight: calculateExportDimensions(currentChartRef).finalHeight,
-                                                    scale: 1
-                                                }, {}))
-                                            } />
-                                        <MenuItem
-                                            isLast={!tableToggle}
-                                            bottomSeparator={!!tableToggle}
-                                            locale={locale}
-                                            prefixIcon={'Download'}
-                                            text={Translations.downloadPNG[locale]}
-                                            onClick={() => handleMenuItemClick(() =>
-                                                currentChartRef.chart.exportChartLocal({
-                                                    filename: `${generateFilename(viewData.tableReferenceName)}`,
-                                                    sourceWidth: calculateExportDimensions(currentChartRef).finalWidth,
-                                                    sourceHeight: calculateExportDimensions(currentChartRef).finalHeight,
-                                                    scale: 1
-                                                }, {}))
-                                            } />
-                                    </>
-                                }
-                                {
-                                    showAccessibilityModeToggle &&
-                                    <MenuItem isFirst={false} locale={locale} text={accessibilityMode ? Translations.toggleAccessibilityModeOff[locale] : Translations.toggleAccessibilityModeOn[locale]} onClick={() => handleMenuItemClick(toggleAccessibilityMode)} />
-                                }
-                                {
-                                    tableToggle &&
-                                    <MenuItem isLast={true} locale={locale} text={tableToggle.tableMode ? Translations.toggleTableModeOffText[locale] : Translations.toggleTableModeOnText[locale]} onClick={() => handleMenuItemClick(tableToggle.toggleHandler)} />
-                                }
+                        <MenuWrapper>
+                            <List id="menu" role="menu" aria-label={`${Translations.chartMenuLabel[locale]}`} aria-orientation="vertical" aria-activedescendant={`menuitem-${selectedIndex}`} tabIndex={isOpen ? 0 : -1}>
path: src/react/components/burgerMenu/burgerMenu.tsx
user: login: JLaiTre
body: The id "menu" seems very generic. It's likely the user of the package has used this id already. And what if they use multiple Charts on the same page? Will they get multiple elements with the same id?
created_at: 2025-04-01T10:37:34Z
author_association: NONE
diff_hunk: @@ -155,80 +308,72 @@ export const BurgerMenu: React.FC<IBurgerMenuProps> = ({ viewData, currentChartR
         }
     }, [isOpen]);
 
+    const getMenuItemMaxIndex = (): number => {
+        const customMenuItemsCount: number = menuItemDefinitions ? menuItemDefinitions.length : 0;
+        return customMenuItemsCount + defaultMenuItemsArray().length - 1;
+    }
+
     const handleKeyDown = (e: KeyboardEvent) => {
-        if (e.key === 'Escape') {
-            setIsOpen(false);
-            buttonRef.current?.focus();
+        if (e.key === 'Escape' ||
+        (e.key === 'Tab' && isOpen)) {
+            toggleMenuOpen(false);
+        }
+        if (e.key === 'ArrowDown' && isOpen) {
+            e.preventDefault();
+            setSelectedIndex((prevIndex) => {
+                const nextIndex = prevIndex + 1;
+                const maxIndex = getMenuItemMaxIndex();
+                return nextIndex > maxIndex ? 0 : nextIndex;
+            });
+        }
+        if (e.key === 'ArrowUp' && isOpen) {
+            e.preventDefault();
+            setSelectedIndex((prevIndex) => {
+                const nextIndex = prevIndex - 1;
+                const maxIndex = getMenuItemMaxIndex();
+                return nextIndex < 0 ? maxIndex : nextIndex;
+            });
         }
     }
 
     const buttonRef = React.useRef<HTMLButtonElement>(null);
     const handleMenuItemClick = (onClick?: () => void) => {
-        setIsOpen(false);
-        buttonRef.current?.focus();
+        toggleMenuOpen(false);
         if (onClick) onClick();
     }
 
+    const toggleMenuOpen = (open: boolean) => {
+        setIsOpen(open);
+    }
+
+    const setFocusToMenuItem = () => {
+        if (selectedIndex === -1) {
+            return;
+        }
+        if (menuItems.current.has(selectedIndex)) {
+            menuItems.current.get(selectedIndex)?.focus();
+        }
+    }
+
     const showAccessibilityModeToggle: boolean =
         !!tableToggle &&
         !tableToggle.tableMode &&
         viewData.visualizationSettings.visualizationType != EVisualizationType.ScatterPlot;
 
     return (
         <BurgerWrapper ref={menuRef}>
-            <Hamburger ref={buttonRef} aria-label={`${Translations.chartMenuLabel[locale]}`} aria-expanded={isOpen} onClick={() => {setIsOpen(!isOpen)}}>
+            <Hamburger ref={buttonRef} aria-label={`${Translations.chartMenuLabel[locale]}`} aria-expanded={isOpen} onClick={() => { toggleMenuOpen(!isOpen) }} aria-haspopup="menu" aria-controls="menu">
path: src/react/components/burgerMenu/burgerMenu.tsx
user: login: JLaiTre
body: These aria-controls attribute needs to use the new generated id. The aria-haspopup="menu" is correct so this doesn't need to be changed (aria-haspopup value is not an id).
created_at: 2025-04-02T10:10:15Z
author_association: NONE
diff_hunk: @@ -155,80 +299,72 @@ export const BurgerMenu: React.FC<IBurgerMenuProps> = ({ viewData, currentChartR
         }
     }, [isOpen]);
 
+    const getMenuItemMaxIndex = (): number => {
+        const customMenuItemsCount: number = menuItemDefinitions ? menuItemDefinitions.length : 0;
+        return customMenuItemsCount + defaultMenuItemsArray().length - 1;
+    }
+
     const handleKeyDown = (e: KeyboardEvent) => {
-        if (e.key === 'Escape') {
-            setIsOpen(false);
-            buttonRef.current?.focus();
+        if (e.key === 'Escape' ||
+        (e.key === 'Tab' && isOpen)) {
+            toggleMenuOpen(false);
+        }
+        if (e.key === 'ArrowDown' && isOpen) {
+            e.preventDefault();
+            setSelectedIndex((prevIndex) => {
+                const nextIndex = prevIndex + 1;
+                const maxIndex = getMenuItemMaxIndex();
+                return nextIndex > maxIndex ? 0 : nextIndex;
+            });
+        }
+        if (e.key === 'ArrowUp' && isOpen) {
+            e.preventDefault();
+            setSelectedIndex((prevIndex) => {
+                const nextIndex = prevIndex - 1;
+                const maxIndex = getMenuItemMaxIndex();
+                return nextIndex < 0 ? maxIndex : nextIndex;
+            });
         }
     }
 
     const buttonRef = React.useRef<HTMLButtonElement>(null);
     const handleMenuItemClick = (onClick?: () => void) => {
-        setIsOpen(false);
-        buttonRef.current?.focus();
+        toggleMenuOpen(false);
         if (onClick) onClick();
     }
 
+    const toggleMenuOpen = (open: boolean) => {
+        setIsOpen(open);
+    }
+
+    const setFocusToMenuItem = () => {
+        if (selectedIndex === -1) {
+            return;
+        }
+        if (menuItems.current.has(selectedIndex)) {
+            menuItems.current.get(selectedIndex)?.focus();
+        }
+    }
+
     const showAccessibilityModeToggle: boolean =
         !!tableToggle &&
         !tableToggle.tableMode &&
         viewData.visualizationSettings.visualizationType != EVisualizationType.ScatterPlot;
 
     return (
         <BurgerWrapper ref={menuRef}>
-            <Hamburger ref={buttonRef} aria-label={`${Translations.chartMenuLabel[locale]}`} aria-expanded={isOpen} onClick={() => {setIsOpen(!isOpen)}}>
+            <Hamburger ref={buttonRef} aria-label={`${Translations.chartMenuLabel[locale]}`} aria-expanded={isOpen} onClick={() => { toggleMenuOpen(!isOpen) }} aria-haspopup="menu" aria-controls="menu">
                 <Icon inheritColor={menuIconInheritColor} icon={isOpen ? 'Times' : 'Bars'} />
             </Hamburger>
             <MenuAnchor>
                 {
                     isOpen &&
-                    <MenuWrapper>
-                        <List>
-                            {menuItemDefinitions && customMenuItemArray}
-                            <MenuItem isFirst={!menuItemDefinitions} locale={locale} prefixIcon={'Download'} text={exportXLSX.text} onClick={() => handleMenuItemClick(exportXLSX.onClick) } />
-                            <MenuItem isLast={!currentChartRef} bottomSeparator={!!currentChartRef} locale={locale} prefixIcon={'Download'} text={exportCSV.text} onClick={() => handleMenuItemClick(exportCSV.onClick) } />
-                            {
-                                    currentChartRef &&
-                                    <>
-                                        <MenuItem
-                                            locale={locale}
-                                            prefixIcon={'Download'}
-                                            text={Translations.downloadSVG[locale]}
-                                            onClick={() => handleMenuItemClick(() =>
-                                                currentChartRef.chart.exportChartLocal({
-                                                    filename: `${generateFilename(viewData.tableReferenceName)}`,
-                                                    type: "image/svg+xml",
-                                                    sourceWidth: calculateExportDimensions(currentChartRef).finalWidth,
-                                                    sourceHeight: calculateExportDimensions(currentChartRef).finalHeight,
-                                                    scale: 1
-                                                }, {}))
-                                            } />
-                                        <MenuItem
-                                            isLast={!tableToggle}
-                                            bottomSeparator={!!tableToggle}
-                                            locale={locale}
-                                            prefixIcon={'Download'}
-                                            text={Translations.downloadPNG[locale]}
-                                            onClick={() => handleMenuItemClick(() =>
-                                                currentChartRef.chart.exportChartLocal({
-                                                    filename: `${generateFilename(viewData.tableReferenceName)}`,
-                                                    sourceWidth: calculateExportDimensions(currentChartRef).finalWidth,
-                                                    sourceHeight: calculateExportDimensions(currentChartRef).finalHeight,
-                                                    scale: 1
-                                                }, {}))
-                                            } />
-                                    </>
-                                }
-                                {
-                                    showAccessibilityModeToggle &&
-                                    <MenuItem isFirst={false} locale={locale} text={accessibilityMode ? Translations.toggleAccessibilityModeOff[locale] : Translations.toggleAccessibilityModeOn[locale]} onClick={() => handleMenuItemClick(toggleAccessibilityMode)} />
-                                }
-                                {
-                                    tableToggle &&
-                                    <MenuItem isLast={true} locale={locale} text={tableToggle.tableMode ? Translations.toggleTableModeOffText[locale] : Translations.toggleTableModeOnText[locale]} onClick={() => handleMenuItemClick(tableToggle.toggleHandler)} />
-                                }
+                        <MenuWrapper>
+                            <List id="menu" role="menu" aria-label={`${Translations.chartMenuLabel[locale]}`} aria-orientation="vertical" aria-activedescendant={`menuitem-${selectedIndex}`} tabIndex={isOpen ? 0 : -1}>
path: src/react/components/burgerMenu/burgerMenu.tsx
user: login: sakari-malkki
body: Changed to use React.useId to solve this
created_at: 2025-04-02T10:29:09Z
author_association: MEMBER
diff_hunk: @@ -155,80 +308,72 @@ export const BurgerMenu: React.FC<IBurgerMenuProps> = ({ viewData, currentChartR
         }
     }, [isOpen]);
 
+    const getMenuItemMaxIndex = (): number => {
+        const customMenuItemsCount: number = menuItemDefinitions ? menuItemDefinitions.length : 0;
+        return customMenuItemsCount + defaultMenuItemsArray().length - 1;
+    }
+
     const handleKeyDown = (e: KeyboardEvent) => {
-        if (e.key === 'Escape') {
-            setIsOpen(false);
-            buttonRef.current?.focus();
+        if (e.key === 'Escape' ||
+        (e.key === 'Tab' && isOpen)) {
+            toggleMenuOpen(false);
+        }
+        if (e.key === 'ArrowDown' && isOpen) {
+            e.preventDefault();
+            setSelectedIndex((prevIndex) => {
+                const nextIndex = prevIndex + 1;
+                const maxIndex = getMenuItemMaxIndex();
+                return nextIndex > maxIndex ? 0 : nextIndex;
+            });
+        }
+        if (e.key === 'ArrowUp' && isOpen) {
+            e.preventDefault();
+            setSelectedIndex((prevIndex) => {
+                const nextIndex = prevIndex - 1;
+                const maxIndex = getMenuItemMaxIndex();
+                return nextIndex < 0 ? maxIndex : nextIndex;
+            });
         }
     }
 
     const buttonRef = React.useRef<HTMLButtonElement>(null);
     const handleMenuItemClick = (onClick?: () => void) => {
-        setIsOpen(false);
-        buttonRef.current?.focus();
+        toggleMenuOpen(false);
         if (onClick) onClick();
     }
 
+    const toggleMenuOpen = (open: boolean) => {
+        setIsOpen(open);
+    }
+
+    const setFocusToMenuItem = () => {
+        if (selectedIndex === -1) {
+            return;
+        }
+        if (menuItems.current.has(selectedIndex)) {
+            menuItems.current.get(selectedIndex)?.focus();
+        }
+    }
+
     const showAccessibilityModeToggle: boolean =
         !!tableToggle &&
         !tableToggle.tableMode &&
         viewData.visualizationSettings.visualizationType != EVisualizationType.ScatterPlot;
 
     return (
         <BurgerWrapper ref={menuRef}>
-            <Hamburger ref={buttonRef} aria-label={`${Translations.chartMenuLabel[locale]}`} aria-expanded={isOpen} onClick={() => {setIsOpen(!isOpen)}}>
+            <Hamburger ref={buttonRef} aria-label={`${Translations.chartMenuLabel[locale]}`} aria-expanded={isOpen} onClick={() => { toggleMenuOpen(!isOpen) }} aria-haspopup="menu" aria-controls="menu">
path: src/react/components/burgerMenu/burgerMenu.tsx
user: login: sakari-malkki
body: Fixed
created_at: 2025-04-02T10:29:20Z
author_association: MEMBER

PR comments:
user: login: sonarqubecloud[bot]
created_at: 2025-04-02T10:27:46Z
body: ## [![Quality Gate Passed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/qg-passed-20px.png 'Quality Gate Passed')](https://sonarcloud.io/dashboard?id=StatisticsFinland_PxVisualizer&pullRequest=93) **Quality Gate passed**  
Issues  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 New issues](https://sonarcloud.io/project/issues?id=StatisticsFinland_PxVisualizer&pullRequest=93&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/accepted-16px.png '') [0 Accepted issues](https://sonarcloud.io/project/issues?id=StatisticsFinland_PxVisualizer&pullRequest=93&issueStatuses=ACCEPTED)

Measures  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=StatisticsFinland_PxVisualizer&pullRequest=93&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [97.8% Coverage on New Code](https://sonarcloud.io/component_measures?id=StatisticsFinland_PxVisualizer&pullRequest=93&metric=new_coverage&view=list)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0.0% Duplication on New Code](https://sonarcloud.io/component_measures?id=StatisticsFinland_PxVisualizer&pullRequest=93&metric=new_duplicated_lines_density&view=list)  
  
[See analysis details on SonarQube Cloud](https://sonarcloud.io/dashboard?id=StatisticsFinland_PxVisualizer&pullRequest=93)


author_association: NONE