feat(ui): arrow key navigation in dropdown (#10033)

Addresses https://codeberg.org/forgejo/forgejo/pulls/7906#issuecomment-5455662, https://codeberg.org/forgejo/forgejo/pulls/10025#issuecomment-8182184

Add arrow navigation to the JS-less/JS-enchanced dropdown component.

Reviewed-on: https://codeberg.org/forgejo/forgejo/pulls/10033
Reviewed-by: Otto <otto@codeberg.org>
Reviewed-by: Gusted <gusted@noreply.codeberg.org>
Co-authored-by: 0ko <0ko@noreply.codeberg.org>
Co-committed-by: 0ko <0ko@noreply.codeberg.org>
This commit is contained in:
0ko 2025-11-10 13:35:38 +01:00 committed by Gusted
parent 53fcf182c5
commit 5c9a909c03
2 changed files with 142 additions and 19 deletions

View file

@ -19,6 +19,7 @@ test('JS enhanced interaction', async ({page}) => {
await expect(nojsNotice).toBeHidden();
// Open and close by clicking summary
const dropdown = page.locator('details.dropdown');
const dropdownSummary = page.locator('details.dropdown > summary');
const dropdownContent = page.locator('details.dropdown > .content');
await expect(dropdownContent).toBeHidden();
@ -37,21 +38,64 @@ test('JS enhanced interaction', async ({page}) => {
// Open and close with keypressing
await dropdownSummary.focus();
// Open with Enter, close with Space
await dropdownSummary.press(`Enter`);
await expect(dropdownContent).toBeVisible();
await dropdownSummary.press(`Space`);
await expect(dropdownContent).toBeHidden();
// Open with Space, close with Enter
await dropdownSummary.press(`Space`);
await expect(dropdownContent).toBeVisible();
await dropdownSummary.press(`Enter`);
await expect(dropdownContent).toBeHidden();
// Open with Enter, close with Enter
await dropdownSummary.press(`Enter`);
await expect(dropdownContent).toBeVisible();
await dropdownSummary.press(`Escape`);
await expect(dropdownContent).toBeHidden();
// Open and navigate with ArrowDown, close with Tab
await dropdownSummary.focus();
await dropdown.press(`ArrowDown`);
await expect(page.locator(`a[href$=".rss"]`)).toBeFocused();
await dropdown.press(`ArrowDown`);
await expect(page.locator(`a[href$=".atom"]`)).toBeFocused();
await dropdown.press(`ArrowDown`);
await expect(page.locator(`a[href$=".keys"]`)).toBeFocused();
await dropdown.press(`ArrowDown`);
await expect(page.locator(`a[href$=".gpg"]`)).toBeFocused();
// ArrowDown won't move us farther than the last dropdown item
await dropdown.press(`ArrowDown`);
await expect(page.locator(`a[href$=".gpg"]`)).toBeFocused();
// Pressing Tab on last item will move us away from the dropdown and close the dropdown
await dropdown.press(`Tab`);
await expect(dropdownContent).toBeHidden();
// Navigate and close with Shift+Tab
await dropdownSummary.focus();
await dropdown.press(`Enter`);
await expect(dropdownSummary).toBeFocused();
await dropdown.press(`ArrowDown`);
await expect(page.locator(`a[href$=".rss"]`)).toBeFocused();
await dropdown.press('Shift+Tab');
await expect(dropdownSummary).toBeFocused();
await dropdown.press('Shift+Tab');
await expect(dropdownContent).toBeHidden();
// Navigate with ArrowUp
await dropdownSummary.focus();
await dropdown.press(`ArrowDown`);
await expect(page.locator(`a[href$=".rss"]`)).toBeFocused();
await dropdown.press(`ArrowDown`);
await expect(page.locator(`a[href$=".atom"]`)).toBeFocused();
await dropdown.press(`ArrowUp`);
await expect(page.locator(`a[href$=".rss"]`)).toBeFocused();
// Pressing ArrowUp on first item will move us to summary, but no farther from here
await dropdown.press(`ArrowUp`);
await expect(dropdownSummary).toBeFocused();
await dropdown.press(`Escape`);
await expect(dropdownContent).toBeHidden();
// Open and then close by opening a different dropdown
const languageMenu = page.locator('.language-menu');
await dropdownSummary.click();
@ -90,17 +134,17 @@ test('No JS interaction', async ({browser}) => {
await expect(dropdownContent).toBeHidden();
// Open and close with keypressing
// Open with Enter, close with Space
await dropdownSummary.press(`Enter`);
await expect(dropdownContent).toBeVisible();
await dropdownSummary.press(`Space`);
await expect(dropdownContent).toBeHidden();
// Open with Space, close with Enter
await dropdownSummary.press(`Space`);
await expect(dropdownContent).toBeVisible();
await dropdownSummary.press(`Enter`);
await expect(dropdownContent).toBeHidden();
// Escape is not usable w/o JS enhancements
// Closing by Escape is not possible w/o JS enhancements
await dropdownSummary.press(`Enter`);
await expect(dropdownContent).toBeVisible();
await dropdownSummary.press(`Escape`);