-
Notifications
You must be signed in to change notification settings - Fork 138
feat(core/tree)- Improve keyboard activation for node selection and expand/collapse for tree-item #2665
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. Weโll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
feat(core/tree)- Improve keyboard activation for node selection and expand/collapse for tree-item #2665
Changes from all commits
3f70e17
3230a8e
4558d88
89b0670
1a03e9b
b250d4e
3a4bd68
d74ef50
89d18c9
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,10 @@ | ||
| --- | ||
| "@siemens/ix": patch | ||
| --- | ||
|
|
||
| fix(core/tree-item): improve keyboard accessibility for tree item interactions | ||
|
|
||
| Tree item chevron and node container are now properly activatable via Enter and | ||
| Space keys. Focus is preserved across tree refreshes. Added `aria-expanded` and | ||
| `aria-disabled` attributes to interactive controls for correct screen-reader | ||
| announcement. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -92,6 +92,27 @@ const updateModel = async (tree: Locator, updatedModel: any) => { | |
| ); | ||
| }; | ||
|
|
||
| regressionTest.describe('accessibility', () => { | ||
| regressionTest('collapsed', async ({ mount, page, makeAxeBuilder }) => { | ||
| await initializeTree(mount, page); | ||
|
|
||
| const accessibilityScanResults = await makeAxeBuilder().analyze(); | ||
| expect(accessibilityScanResults.violations).toEqual([]); | ||
| }); | ||
|
|
||
| regressionTest('expanded', async ({ mount, page, makeAxeBuilder }) => { | ||
| const tree = await initializeTree(mount, page); | ||
|
|
||
| await tree | ||
| .locator('ix-tree-item', { hasText: 'Sample', hasNotText: 'Child' }) | ||
| .locator('ix-icon') | ||
| .click(); | ||
|
|
||
| const accessibilityScanResults = await makeAxeBuilder().analyze(); | ||
| expect(accessibilityScanResults.violations).toEqual([]); | ||
| }); | ||
| }); | ||
|
|
||
| regressionTest('renders', async ({ mount, page }) => { | ||
| const tree = await initializeTree(mount, page); | ||
| const item = tree.locator('ix-tree-item').nth(0); | ||
|
|
@@ -749,3 +770,161 @@ regressionTest( | |
| ); | ||
| } | ||
| ); | ||
|
|
||
| regressionTest( | ||
| 'should expand item when Enter is pressed on chevron', | ||
| async ({ mount, page }) => { | ||
| const tree = await initializeTree(mount, page); | ||
|
|
||
| const sampleItem = tree.locator('ix-tree-item', { | ||
| hasText: 'Sample', | ||
| hasNotText: 'Child', | ||
| }); | ||
| const chevron = sampleItem.locator('ix-icon'); | ||
|
|
||
| await chevron.focus(); | ||
| await page.keyboard.press('Enter'); | ||
|
|
||
| const children = tree.locator('ix-tree-item', { | ||
| hasText: 'Sample Child ', | ||
| }); | ||
| await expect(children.nth(0)).toBeVisible(); | ||
| } | ||
| ); | ||
|
|
||
| regressionTest( | ||
| 'should expand item when Space is pressed on chevron', | ||
| async ({ mount, page }) => { | ||
| const tree = await initializeTree(mount, page); | ||
|
|
||
| const sampleItem = tree.locator('ix-tree-item', { | ||
| hasText: 'Sample', | ||
| hasNotText: 'Child', | ||
| }); | ||
| const chevron = sampleItem.locator('ix-icon'); | ||
|
|
||
| await chevron.focus(); | ||
| await page.keyboard.press('Space'); | ||
|
|
||
| const children = tree.locator('ix-tree-item', { | ||
| hasText: 'Sample Child ', | ||
| }); | ||
| await expect(children.nth(0)).toBeVisible(); | ||
| } | ||
| ); | ||
|
|
||
| regressionTest( | ||
| 'should select item when Enter is pressed on tree-node-container', | ||
| async ({ mount, page }) => { | ||
| const tree = await initializeTree(mount, page); | ||
|
|
||
| const sampleItem = tree.locator('ix-tree-item', { | ||
| hasText: 'Sample', | ||
| hasNotText: 'Child', | ||
| }); | ||
| const container = sampleItem.locator('.tree-node-container'); | ||
|
|
||
| await container.focus(); | ||
| await page.keyboard.press('Enter'); | ||
|
|
||
| await expect(sampleItem).toHaveClass(/selected/); | ||
| } | ||
| ); | ||
|
|
||
| regressionTest( | ||
| 'should select item when Space is pressed on tree-node-container', | ||
| async ({ mount, page }) => { | ||
| const tree = await initializeTree(mount, page); | ||
|
|
||
| const sampleItem = tree.locator('ix-tree-item', { | ||
| hasText: 'Sample', | ||
| hasNotText: 'Child', | ||
| }); | ||
| const container = sampleItem.locator('.tree-node-container'); | ||
|
|
||
| await container.focus(); | ||
| await page.keyboard.press('Space'); | ||
|
|
||
| await expect(sampleItem).toHaveClass(/selected/); | ||
| } | ||
| ); | ||
|
|
||
| regressionTest( | ||
| 'disabled item should not respond to keyboard activation', | ||
| async ({ mount, page }) => { | ||
| await mount(` | ||
| <div style="height: 20rem; width: 100%;"> | ||
| <ix-tree root="root"></ix-tree> | ||
| </div> | ||
| `); | ||
|
|
||
| const tree = page.locator('ix-tree'); | ||
| await tree.evaluate( | ||
| (element: HTMLIxTreeElement, args) => { | ||
| element.model = args.model; | ||
| element.context = args.context; | ||
| }, | ||
| { | ||
| model: { | ||
| root: { | ||
| id: 'root', | ||
| data: { name: '' }, | ||
| hasChildren: true, | ||
| children: ['parent'], | ||
| }, | ||
| parent: { | ||
| id: 'parent', | ||
| data: { name: 'Disabled Parent' }, | ||
| hasChildren: true, | ||
| children: ['child'], | ||
| disabled: true, | ||
| }, | ||
| child: { | ||
| id: 'child', | ||
| data: { name: 'Child' }, | ||
| hasChildren: false, | ||
| children: [], | ||
| }, | ||
| } as TreeModel<unknown>, | ||
| context: { | ||
| root: { isExpanded: true, isSelected: false }, | ||
| parent: { isExpanded: false, isSelected: false }, | ||
| child: { isExpanded: false, isSelected: false }, | ||
| } as TreeContext, | ||
| } | ||
| ); | ||
|
|
||
| await expect(tree).toHaveClass(/hydrated/); | ||
|
|
||
| const parent = tree.locator('ix-tree-item', { hasText: 'Disabled Parent' }); | ||
| const container = parent.locator('.tree-node-container'); | ||
|
|
||
| await container.focus(); | ||
| await page.keyboard.press('Enter'); | ||
|
|
||
| await expect(parent).not.toHaveClass(/selected/); | ||
| await expect( | ||
| tree.locator('ix-tree-item', { hasText: 'Child' }) | ||
| ).not.toBeVisible(); | ||
| } | ||
| ); | ||
|
|
||
| regressionTest( | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ๐ฏ Functional Correctness | ๐ก Minor | โก Quick win Cover chevron focus restoration after refresh. This regression test verifies focus preservation for ๐ค Prompt for AI Agents |
||
| 'should preserve focus on tree-node-container after refreshTree', | ||
| async ({ mount, page }) => { | ||
| const tree = await initializeTree(mount, page); | ||
|
|
||
| const sampleItem = tree.locator('ix-tree-item', { | ||
| hasText: 'Sample', | ||
| hasNotText: 'Child', | ||
| }); | ||
| const container = sampleItem.locator('.tree-node-container'); | ||
|
|
||
| await container.focus(); | ||
| await expect(container).toBeFocused(); | ||
|
|
||
| await tree.evaluate((el: HTMLIxTreeElement) => el.refreshTree()); | ||
|
|
||
| await expect(container).toBeFocused(); | ||
| } | ||
| ); | ||
Uh oh!
There was an error while loading. Please reload this page.