From 4bf2c2e583ab92703706af00f88e8922d7a4ee96 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Thu, 4 Sep 2025 18:47:26 +0000
Subject: [PATCH 1/9] Initial plan
From f2e5fd6f09fce54793fdc2d456d8917c08e748a3 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Thu, 4 Sep 2025 19:11:30 +0000
Subject: [PATCH 2/9] Investigation: Identified root cause of keyboard
navigation issue in custom-select option groups
Co-authored-by: mfranzke <787658+mfranzke@users.noreply.github.com>
---
debug-custom-select.html | 215 ++++++++++++++++++
.../custom-select/custom-select.lite.tsx | 84 ++++---
test-custom-select-fix.html | 109 +++++++++
test-custom-select-group-fix.html | 160 +++++++++++++
4 files changed, 534 insertions(+), 34 deletions(-)
create mode 100644 debug-custom-select.html
create mode 100644 test-custom-select-fix.html
create mode 100644 test-custom-select-group-fix.html
diff --git a/debug-custom-select.html b/debug-custom-select.html
new file mode 100644
index 000000000000..62c453e747f4
--- /dev/null
+++ b/debug-custom-select.html
@@ -0,0 +1,215 @@
+
+
+
+
+
+ Custom Select Debug Test
+
+
+
+ Custom Select Group Navigation Debug
+
+
+
Test Instructions:
+
+ Click on the "Debug" button to show all input elements found
+ Click "Open Select" and then try arrow key navigation
+ Watch the console for debug output
+
+
+
+
+
Option Group Example Structure:
+
+ Select an option
+
+
+
+
+ Debug: Show All Input Elements
+ Open Select
+ Test Navigation Logic
+
+
+
+
+
+
\ No newline at end of file
diff --git a/packages/components/src/components/custom-select/custom-select.lite.tsx b/packages/components/src/components/custom-select/custom-select.lite.tsx
index ec6c04da94c8..5c84e55ec000 100644
--- a/packages/components/src/components/custom-select/custom-select.lite.tsx
+++ b/packages/components/src/components/custom-select/custom-select.lite.tsx
@@ -292,47 +292,63 @@ export default function DBCustomSelect(props: DBCustomSelectProps) {
event.key === 'ArrowDown' ||
event.key === 'ArrowRight'
) {
- if (listElement?.nextElementSibling) {
- listElement?.nextElementSibling
- ?.querySelector('input')
- ?.focus();
- } else {
+ // Find next element with input, skipping group titles
+ let nextElement = listElement?.nextElementSibling;
+ while (nextElement) {
+ const nextInput = nextElement.querySelector('input');
+ if (nextInput) {
+ nextInput.focus();
+ break;
+ }
+ nextElement = nextElement.nextElementSibling;
+ }
+
+ if (!nextElement) {
// We are on the last checkbox we move to the top checkbox
state.handleFocusFirstDropdownCheckbox(
activeElement
);
}
} else {
- if (listElement?.previousElementSibling) {
- listElement?.previousElementSibling
- ?.querySelector('input')
- ?.focus();
- } else if (
- detailsRef.querySelector(
- `input[type="checkbox"]`
- ) !== activeElement
- ) {
- // We are on the top list checkbox but there is a select all checkbox as well
- state.handleFocusFirstDropdownCheckbox(
- activeElement
- );
- } else {
- // We are on the top checkbox, we need to move to the search
- // or to the last checkbox
- const search = getSearchInput(detailsRef);
- if (search) {
- delay(() => {
- search.focus();
- }, 100);
+ // Find previous element with input, skipping group titles
+ let prevElement = listElement?.previousElementSibling;
+ while (prevElement) {
+ const prevInput = prevElement.querySelector('input');
+ if (prevInput) {
+ prevInput.focus();
+ break;
+ }
+ prevElement = prevElement.previousElementSibling;
+ }
+
+ if (!prevElement) {
+ if (
+ detailsRef.querySelector(
+ `input[type="checkbox"]`
+ ) !== activeElement
+ ) {
+ // We are on the top list checkbox but there is a select all checkbox as well
+ state.handleFocusFirstDropdownCheckbox(
+ activeElement
+ );
} else {
- const checkboxList: HTMLInputElement[] =
- Array.from(
- detailsRef?.querySelectorAll(
- `input[type="checkbox"],input[type="radio"]`
- )
- );
- if (checkboxList.length) {
- checkboxList.at(-1)?.focus();
+ // We are on the top checkbox, we need to move to the search
+ // or to the last checkbox
+ const search = getSearchInput(detailsRef);
+ if (search) {
+ delay(() => {
+ search.focus();
+ }, 100);
+ } else {
+ const checkboxList: HTMLInputElement[] =
+ Array.from(
+ detailsRef?.querySelectorAll(
+ `input[type="checkbox"],input[type="radio"]`
+ )
+ );
+ if (checkboxList.length) {
+ checkboxList.at(-1)?.focus();
+ }
}
}
}
diff --git a/test-custom-select-fix.html b/test-custom-select-fix.html
new file mode 100644
index 000000000000..c258b5cc6c69
--- /dev/null
+++ b/test-custom-select-fix.html
@@ -0,0 +1,109 @@
+
+
+
+
+
+ Custom Select Option Groups Test
+
+
+
+
+
+ Custom Select Option Groups Keyboard Navigation Test
+
+
+
Test Instructions:
+
+ Click on the custom select below to open it
+ Use Arrow Down keys to navigate through options
+ Verify you can reach "G2:Option 1" and "G2:Option 2" (options in Option group 2)
+ Use Arrow Up keys to navigate backwards
+ Press Enter or Space to select an option
+
+
Before Fix: Could not navigate to Option group 2 entries
+
After Fix: Should be able to navigate to all options including those in Option group 2
+
+
+
+
Single Select with Option Groups
+
+
+
+
+
+
Multi Select with Option Groups (Reference)
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/test-custom-select-group-fix.html b/test-custom-select-group-fix.html
new file mode 100644
index 000000000000..9c484e2e996f
--- /dev/null
+++ b/test-custom-select-group-fix.html
@@ -0,0 +1,160 @@
+
+
+
+
+
+ Custom Select - Option Groups Keyboard Test
+
+
+
+
+
+
+
Custom Select Keyboard Navigation Test
+
+
+
Instructions:
+
+ Click on the custom select below to open it
+ Try to navigate with Arrow Down/Up keys
+ Notice you can't reach "G2:Option 1" and "G2:Option 2" entries
+ Press Tab key - notice the same issue
+
+
+
+
+
ISSUE: Single Select with Option Groups
+
Keyboard navigation cannot reach Option Group 2 entries
+
+
+
+
+
+
+
+
+
+
+
+
+
Expected Behavior: Arrow keys and Tab should be able to navigate to all radio button options, including those in "Option group 2".
+
Current Problem: Keyboard navigation stops after "G1:Option 2" and cannot reach "G2:Option 1" or "G2:Option 2".
+
+
+
+
+
+
\ No newline at end of file
From 9090fc1f4ebb30efd8b10218dc12a1c4db36cd2e Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Thu, 4 Sep 2025 19:22:25 +0000
Subject: [PATCH 3/9] Fix keyboard navigation issue in custom-select option
groups + add comprehensive test
Co-authored-by: mfranzke <787658+mfranzke@users.noreply.github.com>
---
manual-test-navigation.html | 279 ++++++++++++++++++
.../custom-select-option-groups.spec.tsx | 124 ++++++++
.../custom-select/custom-select.lite.tsx | 7 +-
.../custom-select/custom-select.spec.tsx | 71 +++++
test-navigation-logic.html | 147 +++++++++
5 files changed, 625 insertions(+), 3 deletions(-)
create mode 100644 manual-test-navigation.html
create mode 100644 packages/components/src/components/custom-select/custom-select-option-groups.spec.tsx
create mode 100644 test-navigation-logic.html
diff --git a/manual-test-navigation.html b/manual-test-navigation.html
new file mode 100644
index 000000000000..d186cef6a0de
--- /dev/null
+++ b/manual-test-navigation.html
@@ -0,0 +1,279 @@
+
+
+
+
+
+ Custom Select Issue Reproduction
+
+
+
+ Custom Select Option Groups - Issue Reproduction
+
+
+
Instructions:
+
+ Click on the select to open it
+ Use Arrow Down/Up keys to navigate
+ Notice if you can reach "G2:Option 1" and "G2:Option 2"
+ Watch the console for debug output
+
+
+
+
+
+
+ ▼
+
+
+
+
+
+
+
Debug: Show DOM Structure
+
Test: Navigation Logic
+
+
+
+
+
+
\ No newline at end of file
diff --git a/packages/components/src/components/custom-select/custom-select-option-groups.spec.tsx b/packages/components/src/components/custom-select/custom-select-option-groups.spec.tsx
new file mode 100644
index 000000000000..f547d8850d99
--- /dev/null
+++ b/packages/components/src/components/custom-select/custom-select-option-groups.spec.tsx
@@ -0,0 +1,124 @@
+import AxeBuilder from '@axe-core/playwright';
+import { expect, test } from '@playwright/experimental-ct-react';
+
+import { DBCustomSelect } from './index';
+// @ts-ignore - vue can only find it with .ts as file ending
+import { DEFAULT_VIEWPORT } from '../../shared/constants.ts';
+
+const optionGroupsComp: any = (
+
+);
+
+const optionGroupsMultiple: any = (
+
+);
+
+test.describe('DBCustomSelect - Option Groups Keyboard Navigation', () => {
+ test.beforeEach(async ({ page }) => {
+ await page.setViewportSize(DEFAULT_VIEWPORT);
+ });
+
+ test('single select: should navigate through all options including those in second group', async ({ page, mount }) => {
+ const component = await mount(optionGroupsComp);
+ const summary = component.locator('summary');
+
+ // Open the dropdown and focus first option
+ await page.keyboard.press('Tab');
+ await page.keyboard.press('ArrowDown');
+ await page.waitForTimeout(1000); // wait for focus to apply
+
+ // Should be focused on G1:Option 1
+ const focused1 = await page.evaluate(() => document.activeElement?.value);
+ expect(focused1).toBe('G1:Option 1');
+
+ // Navigate to G1:Option 2
+ await page.keyboard.press('ArrowDown');
+ await page.waitForTimeout(100);
+ const focused2 = await page.evaluate(() => document.activeElement?.value);
+ expect(focused2).toBe('G1:Option 2');
+
+ // THIS IS THE CRITICAL TEST: Navigate from G1:Option 2 to G2:Option 1
+ // This should skip the "Option group 2" title and focus on G2:Option 1
+ await page.keyboard.press('ArrowDown');
+ await page.waitForTimeout(100);
+ const focused3 = await page.evaluate(() => document.activeElement?.value);
+ expect(focused3).toBe('G2:Option 1'); // This is where the bug occurs!
+
+ // Continue to G2:Option 2
+ await page.keyboard.press('ArrowDown');
+ await page.waitForTimeout(100);
+ const focused4 = await page.evaluate(() => document.activeElement?.value);
+ expect(focused4).toBe('G2:Option 2');
+
+ // Test reverse navigation
+ await page.keyboard.press('ArrowUp');
+ await page.waitForTimeout(100);
+ const focused5 = await page.evaluate(() => document.activeElement?.value);
+ expect(focused5).toBe('G2:Option 1');
+
+ await page.keyboard.press('ArrowUp');
+ await page.waitForTimeout(100);
+ const focused6 = await page.evaluate(() => document.activeElement?.value);
+ expect(focused6).toBe('G1:Option 2');
+ });
+
+ test('multiple select: should navigate through all options including those in second group', async ({ page, mount }) => {
+ const component = await mount(optionGroupsMultiple);
+
+ // Open the dropdown and focus first option
+ await page.keyboard.press('Tab');
+ await page.keyboard.press('ArrowDown');
+ await page.waitForTimeout(1000); // wait for focus to apply
+
+ // Should be focused on G1:Option 1
+ const focused1 = await page.evaluate(() => document.activeElement?.value);
+ expect(focused1).toBe('G1:Option 1');
+
+ // Navigate to G1:Option 2
+ await page.keyboard.press('ArrowDown');
+ await page.waitForTimeout(100);
+ const focused2 = await page.evaluate(() => document.activeElement?.value);
+ expect(focused2).toBe('G1:Option 2');
+
+ // THIS IS THE CRITICAL TEST: Navigate from G1:Option 2 to G2:Option 1
+ await page.keyboard.press('ArrowDown');
+ await page.waitForTimeout(100);
+ const focused3 = await page.evaluate(() => document.activeElement?.value);
+ expect(focused3).toBe('G2:Option 1'); // This should work in multiple mode
+
+ // Continue to G2:Option 2
+ await page.keyboard.press('ArrowDown');
+ await page.waitForTimeout(100);
+ const focused4 = await page.evaluate(() => document.activeElement?.value);
+ expect(focused4).toBe('G2:Option 2');
+ });
+
+ test('should be accessible', async ({ mount }) => {
+ const component = await mount(optionGroupsComp);
+ const accessibilityScanResults = await new AxeBuilder({ page }).analyze();
+ expect(accessibilityScanResults.violations).toEqual([]);
+ });
+});
\ No newline at end of file
diff --git a/packages/components/src/components/custom-select/custom-select.lite.tsx b/packages/components/src/components/custom-select/custom-select.lite.tsx
index 5c84e55ec000..53cfe426c0b6 100644
--- a/packages/components/src/components/custom-select/custom-select.lite.tsx
+++ b/packages/components/src/components/custom-select/custom-select.lite.tsx
@@ -322,10 +322,11 @@ export default function DBCustomSelect(props: DBCustomSelectProps) {
}
if (!prevElement) {
+ // Check if we have a "select all" checkbox (only relevant for multi-select)
+ const selectAllCheckbox = detailsRef.querySelector(`input[type="checkbox"]`);
if (
- detailsRef.querySelector(
- `input[type="checkbox"]`
- ) !== activeElement
+ selectAllCheckbox &&
+ selectAllCheckbox !== activeElement
) {
// We are on the top list checkbox but there is a select all checkbox as well
state.handleFocusFirstDropdownCheckbox(
diff --git a/packages/components/src/components/custom-select/custom-select.spec.tsx b/packages/components/src/components/custom-select/custom-select.spec.tsx
index 08080072e638..dc459e671466 100644
--- a/packages/components/src/components/custom-select/custom-select.spec.tsx
+++ b/packages/components/src/components/custom-select/custom-select.spec.tsx
@@ -39,6 +39,21 @@ const selectAllSelect: any = (
placeholder="Placeholder">
);
+const optionGroupsComp: any = (
+
+);
+
const testComponent = () => {
test('should contain text', async ({ mount }) => {
const component = await mount(comp);
@@ -67,6 +82,61 @@ const testA11y = () => {
});
};
+const testOptionGroups = () => {
+ test('option groups single select: should navigate through all options including those in second group', async ({ page, mount }) => {
+ const component = await mount(optionGroupsComp);
+ const summary = component.locator('summary');
+
+ // Open the dropdown and focus first option
+ await page.keyboard.press('Tab');
+ await page.keyboard.press('ArrowDown');
+ await page.waitForTimeout(1000); // wait for focus to apply
+
+ // Should be focused on G1:Option 1
+ const focused1 = await page.evaluate(() => (document.activeElement as HTMLInputElement)?.value);
+ expect(focused1).toBe('G1:Option 1');
+
+ // Navigate to G1:Option 2
+ await page.keyboard.press('ArrowDown');
+ await page.waitForTimeout(100);
+ const focused2 = await page.evaluate(() => (document.activeElement as HTMLInputElement)?.value);
+ expect(focused2).toBe('G1:Option 2');
+
+ // THIS IS THE CRITICAL TEST: Navigate from G1:Option 2 to G2:Option 1
+ // This should skip the "Option group 2" title and focus on G2:Option 1
+ await page.keyboard.press('ArrowDown');
+ await page.waitForTimeout(100);
+ const focused3 = await page.evaluate(() => (document.activeElement as HTMLInputElement)?.value);
+ expect(focused3).toBe('G2:Option 1'); // This is where the bug was!
+
+ // Continue to G2:Option 2
+ await page.keyboard.press('ArrowDown');
+ await page.waitForTimeout(100);
+ const focused4 = await page.evaluate(() => (document.activeElement as HTMLInputElement)?.value);
+ expect(focused4).toBe('G2:Option 2');
+
+ // Test reverse navigation
+ await page.keyboard.press('ArrowUp');
+ await page.waitForTimeout(100);
+ const focused5 = await page.evaluate(() => (document.activeElement as HTMLInputElement)?.value);
+ expect(focused5).toBe('G2:Option 1');
+
+ await page.keyboard.press('ArrowUp');
+ await page.waitForTimeout(100);
+ const focused6 = await page.evaluate(() => (document.activeElement as HTMLInputElement)?.value);
+ expect(focused6).toBe('G1:Option 2');
+ });
+
+ test('option groups: should be accessible', async ({ page, mount }) => {
+ await mount(optionGroupsComp);
+ const accessibilityScanResults = await new AxeBuilder({ page })
+ .include('.db-custom-select')
+ .analyze();
+
+ expect(accessibilityScanResults.violations).toEqual([]);
+ });
+};
+
const testAction = () => {
test('click on single item', async ({ page, mount }) => {
const component = await mount(comp);
@@ -147,4 +217,5 @@ test.describe('DBCustomSelect', () => {
testComponent();
testAction();
testA11y();
+ testOptionGroups();
});
diff --git a/test-navigation-logic.html b/test-navigation-logic.html
new file mode 100644
index 000000000000..0832496d2954
--- /dev/null
+++ b/test-navigation-logic.html
@@ -0,0 +1,147 @@
+
+
+
+
+
+ Test Navigation Logic
+
+
+
+ Testing Custom Select Navigation Logic
+
+
+
Option Group Structure (DOM representation):
+
+
+
+ Test: Navigate DOWN from G1:Option 2
+ Test: Navigate UP from G2:Option 1
+ Test: List All Inputs
+
+
+
+
+
+
\ No newline at end of file
From 23565b284339f0af7c26397c76094d52cb1d0329 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Thu, 4 Sep 2025 19:26:01 +0000
Subject: [PATCH 4/9] Final validation and cleanup - fix confirmed working
Co-authored-by: mfranzke <787658+mfranzke@users.noreply.github.com>
---
debug-custom-select.html | 215 --------------
manual-test-navigation.html | 279 ------------------
.../custom-select-option-groups.spec.tsx | 124 --------
test-custom-select-fix.html | 109 -------
test-custom-select-group-fix.html | 160 ----------
test-navigation-logic.html | 147 ---------
6 files changed, 1034 deletions(-)
delete mode 100644 debug-custom-select.html
delete mode 100644 manual-test-navigation.html
delete mode 100644 packages/components/src/components/custom-select/custom-select-option-groups.spec.tsx
delete mode 100644 test-custom-select-fix.html
delete mode 100644 test-custom-select-group-fix.html
delete mode 100644 test-navigation-logic.html
diff --git a/debug-custom-select.html b/debug-custom-select.html
deleted file mode 100644
index 62c453e747f4..000000000000
--- a/debug-custom-select.html
+++ /dev/null
@@ -1,215 +0,0 @@
-
-
-
-
-
- Custom Select Debug Test
-
-
-
- Custom Select Group Navigation Debug
-
-
-
Test Instructions:
-
- Click on the "Debug" button to show all input elements found
- Click "Open Select" and then try arrow key navigation
- Watch the console for debug output
-
-
-
-
-
Option Group Example Structure:
-
- Select an option
-
-
-
-
- Debug: Show All Input Elements
- Open Select
- Test Navigation Logic
-
-
-
-
-
-
\ No newline at end of file
diff --git a/manual-test-navigation.html b/manual-test-navigation.html
deleted file mode 100644
index d186cef6a0de..000000000000
--- a/manual-test-navigation.html
+++ /dev/null
@@ -1,279 +0,0 @@
-
-
-
-
-
- Custom Select Issue Reproduction
-
-
-
- Custom Select Option Groups - Issue Reproduction
-
-
-
Instructions:
-
- Click on the select to open it
- Use Arrow Down/Up keys to navigate
- Notice if you can reach "G2:Option 1" and "G2:Option 2"
- Watch the console for debug output
-
-
-
-
-
-
- ▼
-
-
-
-
-
-
-
Debug: Show DOM Structure
-
Test: Navigation Logic
-
-
-
-
-
-
\ No newline at end of file
diff --git a/packages/components/src/components/custom-select/custom-select-option-groups.spec.tsx b/packages/components/src/components/custom-select/custom-select-option-groups.spec.tsx
deleted file mode 100644
index f547d8850d99..000000000000
--- a/packages/components/src/components/custom-select/custom-select-option-groups.spec.tsx
+++ /dev/null
@@ -1,124 +0,0 @@
-import AxeBuilder from '@axe-core/playwright';
-import { expect, test } from '@playwright/experimental-ct-react';
-
-import { DBCustomSelect } from './index';
-// @ts-ignore - vue can only find it with .ts as file ending
-import { DEFAULT_VIEWPORT } from '../../shared/constants.ts';
-
-const optionGroupsComp: any = (
-
-);
-
-const optionGroupsMultiple: any = (
-
-);
-
-test.describe('DBCustomSelect - Option Groups Keyboard Navigation', () => {
- test.beforeEach(async ({ page }) => {
- await page.setViewportSize(DEFAULT_VIEWPORT);
- });
-
- test('single select: should navigate through all options including those in second group', async ({ page, mount }) => {
- const component = await mount(optionGroupsComp);
- const summary = component.locator('summary');
-
- // Open the dropdown and focus first option
- await page.keyboard.press('Tab');
- await page.keyboard.press('ArrowDown');
- await page.waitForTimeout(1000); // wait for focus to apply
-
- // Should be focused on G1:Option 1
- const focused1 = await page.evaluate(() => document.activeElement?.value);
- expect(focused1).toBe('G1:Option 1');
-
- // Navigate to G1:Option 2
- await page.keyboard.press('ArrowDown');
- await page.waitForTimeout(100);
- const focused2 = await page.evaluate(() => document.activeElement?.value);
- expect(focused2).toBe('G1:Option 2');
-
- // THIS IS THE CRITICAL TEST: Navigate from G1:Option 2 to G2:Option 1
- // This should skip the "Option group 2" title and focus on G2:Option 1
- await page.keyboard.press('ArrowDown');
- await page.waitForTimeout(100);
- const focused3 = await page.evaluate(() => document.activeElement?.value);
- expect(focused3).toBe('G2:Option 1'); // This is where the bug occurs!
-
- // Continue to G2:Option 2
- await page.keyboard.press('ArrowDown');
- await page.waitForTimeout(100);
- const focused4 = await page.evaluate(() => document.activeElement?.value);
- expect(focused4).toBe('G2:Option 2');
-
- // Test reverse navigation
- await page.keyboard.press('ArrowUp');
- await page.waitForTimeout(100);
- const focused5 = await page.evaluate(() => document.activeElement?.value);
- expect(focused5).toBe('G2:Option 1');
-
- await page.keyboard.press('ArrowUp');
- await page.waitForTimeout(100);
- const focused6 = await page.evaluate(() => document.activeElement?.value);
- expect(focused6).toBe('G1:Option 2');
- });
-
- test('multiple select: should navigate through all options including those in second group', async ({ page, mount }) => {
- const component = await mount(optionGroupsMultiple);
-
- // Open the dropdown and focus first option
- await page.keyboard.press('Tab');
- await page.keyboard.press('ArrowDown');
- await page.waitForTimeout(1000); // wait for focus to apply
-
- // Should be focused on G1:Option 1
- const focused1 = await page.evaluate(() => document.activeElement?.value);
- expect(focused1).toBe('G1:Option 1');
-
- // Navigate to G1:Option 2
- await page.keyboard.press('ArrowDown');
- await page.waitForTimeout(100);
- const focused2 = await page.evaluate(() => document.activeElement?.value);
- expect(focused2).toBe('G1:Option 2');
-
- // THIS IS THE CRITICAL TEST: Navigate from G1:Option 2 to G2:Option 1
- await page.keyboard.press('ArrowDown');
- await page.waitForTimeout(100);
- const focused3 = await page.evaluate(() => document.activeElement?.value);
- expect(focused3).toBe('G2:Option 1'); // This should work in multiple mode
-
- // Continue to G2:Option 2
- await page.keyboard.press('ArrowDown');
- await page.waitForTimeout(100);
- const focused4 = await page.evaluate(() => document.activeElement?.value);
- expect(focused4).toBe('G2:Option 2');
- });
-
- test('should be accessible', async ({ mount }) => {
- const component = await mount(optionGroupsComp);
- const accessibilityScanResults = await new AxeBuilder({ page }).analyze();
- expect(accessibilityScanResults.violations).toEqual([]);
- });
-});
\ No newline at end of file
diff --git a/test-custom-select-fix.html b/test-custom-select-fix.html
deleted file mode 100644
index c258b5cc6c69..000000000000
--- a/test-custom-select-fix.html
+++ /dev/null
@@ -1,109 +0,0 @@
-
-
-
-
-
- Custom Select Option Groups Test
-
-
-
-
-
- Custom Select Option Groups Keyboard Navigation Test
-
-
-
Test Instructions:
-
- Click on the custom select below to open it
- Use Arrow Down keys to navigate through options
- Verify you can reach "G2:Option 1" and "G2:Option 2" (options in Option group 2)
- Use Arrow Up keys to navigate backwards
- Press Enter or Space to select an option
-
-
Before Fix: Could not navigate to Option group 2 entries
-
After Fix: Should be able to navigate to all options including those in Option group 2
-
-
-
-
Single Select with Option Groups
-
-
-
-
-
-
Multi Select with Option Groups (Reference)
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/test-custom-select-group-fix.html b/test-custom-select-group-fix.html
deleted file mode 100644
index 9c484e2e996f..000000000000
--- a/test-custom-select-group-fix.html
+++ /dev/null
@@ -1,160 +0,0 @@
-
-
-
-
-
- Custom Select - Option Groups Keyboard Test
-
-
-
-
-
-
-
Custom Select Keyboard Navigation Test
-
-
-
Instructions:
-
- Click on the custom select below to open it
- Try to navigate with Arrow Down/Up keys
- Notice you can't reach "G2:Option 1" and "G2:Option 2" entries
- Press Tab key - notice the same issue
-
-
-
-
-
ISSUE: Single Select with Option Groups
-
Keyboard navigation cannot reach Option Group 2 entries
-
-
-
-
-
-
-
-
-
-
-
-
-
Expected Behavior: Arrow keys and Tab should be able to navigate to all radio button options, including those in "Option group 2".
-
Current Problem: Keyboard navigation stops after "G1:Option 2" and cannot reach "G2:Option 1" or "G2:Option 2".
-
-
-
-
-
-
\ No newline at end of file
diff --git a/test-navigation-logic.html b/test-navigation-logic.html
deleted file mode 100644
index 0832496d2954..000000000000
--- a/test-navigation-logic.html
+++ /dev/null
@@ -1,147 +0,0 @@
-
-
-
-
-
- Test Navigation Logic
-
-
-
- Testing Custom Select Navigation Logic
-
-
-
Option Group Structure (DOM representation):
-
-
-
- Test: Navigate DOWN from G1:Option 2
- Test: Navigate UP from G2:Option 1
- Test: List All Inputs
-
-
-
-
-
-
\ No newline at end of file
From 12ccbdb1ff2b823a68b9358c216bd612db95411b Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Thu, 4 Sep 2025 19:40:06 +0000
Subject: [PATCH 5/9] Consolidate option groups tests into existing test
functions
Co-authored-by: mfranzke <787658+mfranzke@users.noreply.github.com>
---
.../custom-select/custom-select.spec.tsx | 96 +++++++++----------
1 file changed, 47 insertions(+), 49 deletions(-)
diff --git a/packages/components/src/components/custom-select/custom-select.spec.tsx b/packages/components/src/components/custom-select/custom-select.spec.tsx
index dc459e671466..a0a9ca4e0ce4 100644
--- a/packages/components/src/components/custom-select/custom-select.spec.tsx
+++ b/packages/components/src/components/custom-select/custom-select.spec.tsx
@@ -80,54 +80,7 @@ const testA11y = () => {
expect(accessibilityScanResults.violations).toEqual([]);
});
-};
-
-const testOptionGroups = () => {
- test('option groups single select: should navigate through all options including those in second group', async ({ page, mount }) => {
- const component = await mount(optionGroupsComp);
- const summary = component.locator('summary');
-
- // Open the dropdown and focus first option
- await page.keyboard.press('Tab');
- await page.keyboard.press('ArrowDown');
- await page.waitForTimeout(1000); // wait for focus to apply
-
- // Should be focused on G1:Option 1
- const focused1 = await page.evaluate(() => (document.activeElement as HTMLInputElement)?.value);
- expect(focused1).toBe('G1:Option 1');
-
- // Navigate to G1:Option 2
- await page.keyboard.press('ArrowDown');
- await page.waitForTimeout(100);
- const focused2 = await page.evaluate(() => (document.activeElement as HTMLInputElement)?.value);
- expect(focused2).toBe('G1:Option 2');
-
- // THIS IS THE CRITICAL TEST: Navigate from G1:Option 2 to G2:Option 1
- // This should skip the "Option group 2" title and focus on G2:Option 1
- await page.keyboard.press('ArrowDown');
- await page.waitForTimeout(100);
- const focused3 = await page.evaluate(() => (document.activeElement as HTMLInputElement)?.value);
- expect(focused3).toBe('G2:Option 1'); // This is where the bug was!
-
- // Continue to G2:Option 2
- await page.keyboard.press('ArrowDown');
- await page.waitForTimeout(100);
- const focused4 = await page.evaluate(() => (document.activeElement as HTMLInputElement)?.value);
- expect(focused4).toBe('G2:Option 2');
-
- // Test reverse navigation
- await page.keyboard.press('ArrowUp');
- await page.waitForTimeout(100);
- const focused5 = await page.evaluate(() => (document.activeElement as HTMLInputElement)?.value);
- expect(focused5).toBe('G2:Option 1');
-
- await page.keyboard.press('ArrowUp');
- await page.waitForTimeout(100);
- const focused6 = await page.evaluate(() => (document.activeElement as HTMLInputElement)?.value);
- expect(focused6).toBe('G1:Option 2');
- });
-
- test('option groups: should be accessible', async ({ page, mount }) => {
+ test('option groups should be accessible', async ({ page, mount }) => {
await mount(optionGroupsComp);
const accessibilityScanResults = await new AxeBuilder({ page })
.include('.db-custom-select')
@@ -210,12 +163,57 @@ const testAction = () => {
await page.keyboard.press('Escape');
await expect(summary).toContainText('Option 1');
});
+
+ test('option groups keyboard navigation: should navigate between option groups correctly', async ({ page, mount }) => {
+ const component = await mount(optionGroupsComp);
+
+ // Open the dropdown and focus first option
+ await page.keyboard.press('Tab');
+ await page.keyboard.press('ArrowDown');
+ await page.waitForTimeout(1000); // wait for focus to apply
+
+ // Should be focused on G1:Option 1
+ const focused1 = await page.evaluate(() => (document.activeElement as HTMLInputElement)?.value);
+ expect(focused1).toBe('G1:Option 1');
+
+ // Navigate to G1:Option 2
+ await page.keyboard.press('ArrowDown');
+ await page.waitForTimeout(100);
+ const focused2 = await page.evaluate(() => (document.activeElement as HTMLInputElement)?.value);
+ expect(focused2).toBe('G1:Option 2');
+
+ // CRITICAL TEST: Navigate from G1:Option 2 to G2:Option 1
+ // This should skip the "Option group 2" title and focus on G2:Option 1
+ // This is the core issue that was fixed in #4920
+ await page.keyboard.press('ArrowDown');
+ await page.waitForTimeout(100);
+ const focused3 = await page.evaluate(() => (document.activeElement as HTMLInputElement)?.value);
+ expect(focused3).toBe('G2:Option 1'); // This was previously broken
+
+ // Continue to G2:Option 2
+ await page.keyboard.press('ArrowDown');
+ await page.waitForTimeout(100);
+ const focused4 = await page.evaluate(() => (document.activeElement as HTMLInputElement)?.value);
+ expect(focused4).toBe('G2:Option 2');
+
+ // Test reverse navigation
+ await page.keyboard.press('ArrowUp');
+ await page.waitForTimeout(100);
+ const focused5 = await page.evaluate(() => (document.activeElement as HTMLInputElement)?.value);
+ expect(focused5).toBe('G2:Option 1');
+
+ await page.keyboard.press('ArrowUp');
+ await page.waitForTimeout(100);
+ const focused6 = await page.evaluate(() => (document.activeElement as HTMLInputElement)?.value);
+ expect(focused6).toBe('G1:Option 2');
+ });
};
+
+
test.describe('DBCustomSelect', () => {
test.use({ viewport: DEFAULT_VIEWPORT });
testComponent();
testAction();
testA11y();
- testOptionGroups();
});
From 38459196e9570217182ab7d34b3838d1a2346522 Mon Sep 17 00:00:00 2001
From: Maximilian Franzke <787658+mfranzke@users.noreply.github.com>
Date: Mon, 8 Sep 2025 08:04:51 +0200
Subject: [PATCH 6/9] Update
packages/components/src/components/custom-select/custom-select.spec.tsx
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
---
.../src/components/custom-select/custom-select.spec.tsx | 2 --
1 file changed, 2 deletions(-)
diff --git a/packages/components/src/components/custom-select/custom-select.spec.tsx b/packages/components/src/components/custom-select/custom-select.spec.tsx
index a0a9ca4e0ce4..1d8cb29cade7 100644
--- a/packages/components/src/components/custom-select/custom-select.spec.tsx
+++ b/packages/components/src/components/custom-select/custom-select.spec.tsx
@@ -209,8 +209,6 @@ const testAction = () => {
});
};
-
-
test.describe('DBCustomSelect', () => {
test.use({ viewport: DEFAULT_VIEWPORT });
testComponent();
From 69c1bc963102b071f3799c592156cea4de2b2636 Mon Sep 17 00:00:00 2001
From: Copilot
Date: Mon, 8 Sep 2025 06:41:25 +0000
Subject: [PATCH 7/9] test: replace hard-coded timeouts with reliable
waitForFocusChange helper function
---
.../custom-select/custom-select.spec.tsx | 81 +++++++++++++------
1 file changed, 56 insertions(+), 25 deletions(-)
diff --git a/packages/components/src/components/custom-select/custom-select.spec.tsx b/packages/components/src/components/custom-select/custom-select.spec.tsx
index 1d8cb29cade7..90209a81f2ed 100644
--- a/packages/components/src/components/custom-select/custom-select.spec.tsx
+++ b/packages/components/src/components/custom-select/custom-select.spec.tsx
@@ -5,6 +5,22 @@ import { DBCustomSelect } from './index';
// @ts-ignore - vue can only find it with .ts as file ending
import { DEFAULT_VIEWPORT } from '../../shared/constants.ts';
+// Helper function to wait for focus changes instead of using hard-coded timeouts
+const waitForFocusChange = async (
+ page: any,
+ expectedValue: string,
+ timeout = 5000
+) => {
+ await page.waitForFunction(
+ (expected) => {
+ const activeElement = document.activeElement as HTMLInputElement;
+ return activeElement && activeElement.value === expected;
+ },
+ expectedValue,
+ { timeout }
+ );
+};
+
const comp: any = (
{
await expect(summary).not.toContainText('Option 1');
await page.keyboard.press('Tab');
await page.keyboard.press('ArrowDown');
- await page.waitForTimeout(1000); // wait for focus to apply
+ await waitForFocusChange(page, 'Option 1');
await page.keyboard.press('Space');
await expect(summary).toContainText('Option 1');
});
@@ -108,7 +124,7 @@ const testAction = () => {
await expect(summary).not.toContainText('Option 1');
await page.keyboard.press('Tab');
await page.keyboard.press('ArrowDown');
- await page.waitForTimeout(1000); // wait for focus to apply
+ await waitForFocusChange(page, 'Option 1');
await page.keyboard.press('Space');
await page.keyboard.press('Escape');
await expect(summary).toContainText('Option 1');
@@ -131,7 +147,7 @@ const testAction = () => {
const summary = component.locator('summary');
await page.keyboard.press('Tab');
await page.keyboard.press('ArrowDown');
- await page.waitForTimeout(1000); // wait for focus to apply
+ await waitForFocusChange(page, 'Option 1');
await page.keyboard.press('Space');
await page.keyboard.press('Escape');
await expect(summary).toContainText('Option 1, Option 2');
@@ -143,7 +159,7 @@ const testAction = () => {
await expect(summary).not.toContainText('Option 1');
await page.keyboard.press('Tab');
await page.keyboard.press('ArrowDown');
- await page.waitForTimeout(1000); // wait for focus to apply
+ await waitForFocusChange(page, 'Option 1');
await page.keyboard.press('Enter');
await expect(summary).toContainText('Option 1');
// For single select, dropdown should be closed after Enter
@@ -156,7 +172,7 @@ const testAction = () => {
await expect(summary).not.toContainText('Option 1');
await page.keyboard.press('Tab');
await page.keyboard.press('ArrowDown');
- await page.waitForTimeout(1000); // wait for focus to apply
+ await waitForFocusChange(page, 'Option 1');
await page.keyboard.press('Enter');
// For multiple select, dropdown should remain open after Enter
await expect(component.locator('details')).toHaveAttribute('open');
@@ -164,47 +180,62 @@ const testAction = () => {
await expect(summary).toContainText('Option 1');
});
- test('option groups keyboard navigation: should navigate between option groups correctly', async ({ page, mount }) => {
+ test('option groups keyboard navigation: should navigate between option groups correctly', async ({
+ page,
+ mount
+ }) => {
const component = await mount(optionGroupsComp);
-
+
// Open the dropdown and focus first option
await page.keyboard.press('Tab');
await page.keyboard.press('ArrowDown');
- await page.waitForTimeout(1000); // wait for focus to apply
-
+ await waitForFocusChange(page, 'G1:Option 1');
+
// Should be focused on G1:Option 1
- const focused1 = await page.evaluate(() => (document.activeElement as HTMLInputElement)?.value);
+ const focused1 = await page.evaluate(
+ () => (document.activeElement as HTMLInputElement)?.value
+ );
expect(focused1).toBe('G1:Option 1');
-
+
// Navigate to G1:Option 2
await page.keyboard.press('ArrowDown');
- await page.waitForTimeout(100);
- const focused2 = await page.evaluate(() => (document.activeElement as HTMLInputElement)?.value);
+ await waitForFocusChange(page, 'G1:Option 2');
+ const focused2 = await page.evaluate(
+ () => (document.activeElement as HTMLInputElement)?.value
+ );
expect(focused2).toBe('G1:Option 2');
-
+
// CRITICAL TEST: Navigate from G1:Option 2 to G2:Option 1
// This should skip the "Option group 2" title and focus on G2:Option 1
// This is the core issue that was fixed in #4920
await page.keyboard.press('ArrowDown');
- await page.waitForTimeout(100);
- const focused3 = await page.evaluate(() => (document.activeElement as HTMLInputElement)?.value);
+ await waitForFocusChange(page, 'G2:Option 1');
+ const focused3 = await page.evaluate(
+ () => (document.activeElement as HTMLInputElement)?.value
+ );
expect(focused3).toBe('G2:Option 1'); // This was previously broken
-
+
// Continue to G2:Option 2
await page.keyboard.press('ArrowDown');
- await page.waitForTimeout(100);
- const focused4 = await page.evaluate(() => (document.activeElement as HTMLInputElement)?.value);
+ await waitForFocusChange(page, 'G2:Option 2');
+ const focused4 = await page.evaluate(
+ () => (document.activeElement as HTMLInputElement)?.value
+ );
expect(focused4).toBe('G2:Option 2');
-
+
// Test reverse navigation
await page.keyboard.press('ArrowUp');
- await page.waitForTimeout(100);
- const focused5 = await page.evaluate(() => (document.activeElement as HTMLInputElement)?.value);
+ await waitForFocusChange(page, 'G2:Option 1');
+ const focused5 = await page.evaluate(
+ () => (document.activeElement as HTMLInputElement)?.value
+ );
expect(focused5).toBe('G2:Option 1');
-
+
await page.keyboard.press('ArrowUp');
- await page.waitForTimeout(100);
- const focused6 = await page.evaluate(() => (document.activeElement as HTMLInputElement)?.value);
+ await waitForFocusChange(page, 'G1:Option 2');
+ const focused6 = await page.evaluate(
+ () => (document.activeElement as HTMLInputElement)?.value
+ );
expect(focused6).toBe('G1:Option 2');
});
};
From 7ed7d37077b6384c7c39bf44b5f4e1543549f8bd Mon Sep 17 00:00:00 2001
From: Nicolas Merget
Date: Mon, 29 Sep 2025 13:26:08 +0200
Subject: [PATCH 8/9] fix: issue with test in custom-select.spec.tsx
---
.../src/components/custom-select/custom-select.spec.tsx | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/packages/components/src/components/custom-select/custom-select.spec.tsx b/packages/components/src/components/custom-select/custom-select.spec.tsx
index 90209a81f2ed..a71a9d0bf8a2 100644
--- a/packages/components/src/components/custom-select/custom-select.spec.tsx
+++ b/packages/components/src/components/custom-select/custom-select.spec.tsx
@@ -12,7 +12,7 @@ const waitForFocusChange = async (
timeout = 5000
) => {
await page.waitForFunction(
- (expected) => {
+ (expected: any) => {
const activeElement = document.activeElement as HTMLInputElement;
return activeElement && activeElement.value === expected;
},
@@ -147,7 +147,7 @@ const testAction = () => {
const summary = component.locator('summary');
await page.keyboard.press('Tab');
await page.keyboard.press('ArrowDown');
- await waitForFocusChange(page, 'Option 1');
+ await page.waitForTimeout(1000); // wait for focus to apply
await page.keyboard.press('Space');
await page.keyboard.press('Escape');
await expect(summary).toContainText('Option 1, Option 2');
From ffd1562254e3581b62682c8a11c6d1019f09a960 Mon Sep 17 00:00:00 2001
From: Nicolas Merget
Date: Mon, 29 Sep 2025 13:44:56 +0200
Subject: [PATCH 9/9] chore: add changeset
---
.changeset/keyboard-navigation-option-groups.md | 13 +++++++++++++
1 file changed, 13 insertions(+)
create mode 100644 .changeset/keyboard-navigation-option-groups.md
diff --git a/.changeset/keyboard-navigation-option-groups.md b/.changeset/keyboard-navigation-option-groups.md
new file mode 100644
index 000000000000..8f8994ebaf1b
--- /dev/null
+++ b/.changeset/keyboard-navigation-option-groups.md
@@ -0,0 +1,13 @@
+---
+"@db-ux/core-components": patch
+"@db-ux/ngx-core-components": patch
+"@db-ux/react-core-components": patch
+"@db-ux/v-core-components": patch
+"@db-ux/wc-core-components": patch
+---
+
+- fix(custom-select): keyboard navigation for option groups in single-select mode:
+ - Fixes a keyboard accessibility issue where users could not navigate to options in subsequent option groups using arrow keys in single-select mode.
+ - Now, all options are accessible via keyboard regardless of group boundaries.
+
+