Skip to content
Open
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/dropdown-button-decorative-icon.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@siemens/ix': patch
---

Fix `ix-dropdown-button` accessibility by hiding decorative content and exposing only the host as the interactive, named menu button.
Comment thread
dmytro-halimov marked this conversation as resolved.
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
.dropdown-button {
display: flex;
flex-direction: column;
align-items: flex-start;
justify-content: center;
position: relative;
max-width: 20rem;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,19 +18,27 @@ import {
imports: [IxDropdownButton, IxDropdownItem],
template: `
<div class="example">
<ix-dropdown-button label="Dropdown" icon="checkboxes">
<ix-dropdown-button label="Primary dropdown" icon="checkboxes">
<ix-dropdown-item label="Item 1"></ix-dropdown-item>
<ix-dropdown-item label="Item 2"></ix-dropdown-item>
</ix-dropdown-button>
<ix-dropdown-button variant="subtle-primary" label="Dropdown" icon="checkboxes">
<ix-dropdown-button
variant="subtle-primary"
label="Subtle primary dropdown"
icon="checkboxes"
>
<ix-dropdown-item label="Item 1"></ix-dropdown-item>
<ix-dropdown-item label="Item 2"></ix-dropdown-item>
</ix-dropdown-button>
<ix-dropdown-button variant="tertiary" label="Dropdown" icon="checkboxes">
<ix-dropdown-button
variant="tertiary"
label="Tertiary dropdown"
icon="checkboxes"
>
<ix-dropdown-item label="Item 1"></ix-dropdown-item>
<ix-dropdown-item label="Item 2"></ix-dropdown-item>
</ix-dropdown-button>
<ix-dropdown-button label="Dropdown" disabled icon="checkboxes">
<ix-dropdown-button label="Disabled dropdown" disabled icon="checkboxes">
Comment thread
dmytro-halimov marked this conversation as resolved.
</ix-dropdown-button>
</div>
`,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
.dropdown-button {
display: flex;
flex-direction: column;
align-items: flex-start;
justify-content: center;
position: relative;
max-width: 20rem;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,19 +14,27 @@ import { Component } from '@angular/core';
selector: 'app-example',
template: `
<div class="example">
<ix-dropdown-button label="Dropdown" icon="checkboxes">
<ix-dropdown-button label="Primary dropdown" icon="checkboxes">
<ix-dropdown-item label="Item 1"></ix-dropdown-item>
<ix-dropdown-item label="Item 2"></ix-dropdown-item>
</ix-dropdown-button>
<ix-dropdown-button variant="subtle-primary" label="Dropdown" icon="checkboxes">
<ix-dropdown-button
variant="subtle-primary"
label="Subtle primary dropdown"
icon="checkboxes"
>
<ix-dropdown-item label="Item 1"></ix-dropdown-item>
<ix-dropdown-item label="Item 2"></ix-dropdown-item>
</ix-dropdown-button>
<ix-dropdown-button variant="tertiary" label="Dropdown" icon="checkboxes">
<ix-dropdown-button
variant="tertiary"
label="Tertiary dropdown"
icon="checkboxes"
>
<ix-dropdown-item label="Item 1"></ix-dropdown-item>
<ix-dropdown-item label="Item 2"></ix-dropdown-item>
</ix-dropdown-button>
<ix-dropdown-button label="Dropdown" disabled icon="checkboxes">
<ix-dropdown-button label="Disabled dropdown" disabled icon="checkboxes">
</ix-dropdown-button>
</div>
`,
Expand Down
200 changes: 195 additions & 5 deletions packages/core/src/components/dropdown-button/dropdown-button.ct.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,17 +7,207 @@
* LICENSE file in the root directory of this source tree.
*/
import { expect } from '@playwright/test';
import { iconRocket } from '@siemens/ix-icons/icons';
import { regressionTest } from '@utils/test';

regressionTest('renders', async ({ mount, page }) => {
regressionTest('accessibility', async ({ mount, makeAxeBuilder, page }) => {
await mount(`
<ix-dropdown-button label="Open">
<ix-dropdown-item label="Test"></ix-dropdown-item>
</ix-dropdown-button>
<ix-dropdown-button label="Actions">
<ix-dropdown-item label="Edit"></ix-dropdown-item>
<ix-dropdown-item label="Delete"></ix-dropdown-item>
</ix-dropdown-button>
`);

await expect(page.locator('ix-dropdown-button')).toHaveClass(/\bhydrated\b/);

const accessibilityScanResults = await makeAxeBuilder().analyze();
expect(accessibilityScanResults.violations).toEqual([]);
});

regressionTest(
'renders labeled and icon-only variants',
async ({ mount, page }) => {
await mount(
`
<ix-dropdown-button id="labeled-button" label="Actions">
<ix-dropdown-item label="Edit"></ix-dropdown-item>
</ix-dropdown-button>
<ix-dropdown-button
id="icon-button"
icon="rocket"
aria-label-dropdown-button="Launch actions"
>
<ix-dropdown-item label="Launch"></ix-dropdown-item>
</ix-dropdown-button>
`,
{ icons: { iconRocket } }
);

const labeledButton = page.locator('#labeled-button');
const iconButton = page.locator('#icon-button');

await expect(labeledButton).toHaveClass(/\bhydrated\b/);
await expect(labeledButton).toBeVisible();
await expect(labeledButton).toHaveAccessibleName('Actions');
await expect(labeledButton.locator('ix-button')).toHaveClass(
/\bhydrated\b/
);

await expect(iconButton).toHaveClass(/\bhydrated\b/);
await expect(iconButton).toBeVisible();
await expect(iconButton).toHaveAccessibleName('Launch actions');
await expect(iconButton.locator('ix-icon-button')).toHaveClass(
/\bhydrated\b/
);
}
);

regressionTest(
'uses the expected accessible-label precedence',
async ({ mount, page }) => {
await mount(`
<ix-dropdown-button
id="host-label"
aria-label="Host label"
aria-label-dropdown-button="Dropdown button label"
label="Visible label"
>
<ix-dropdown-item label="Item"></ix-dropdown-item>
</ix-dropdown-button>
<ix-dropdown-button
id="dropdown-button-label"
aria-label-dropdown-button="Dropdown button label"
label="Visible label"
>
<ix-dropdown-item label="Item"></ix-dropdown-item>
</ix-dropdown-button>
<ix-dropdown-button id="visible-label" label="Visible label">
<ix-dropdown-item label="Item"></ix-dropdown-item>
</ix-dropdown-button>
`);

await expect(page.locator('#host-label')).toHaveAccessibleName(
'Host label'
);
await expect(page.locator('#dropdown-button-label')).toHaveAccessibleName(
'Dropdown button label'
);
await expect(page.locator('#visible-label')).toHaveAccessibleName(
'Visible label'
);
}
);

regressionTest('keeps focus on the host button', async ({ mount, page }) => {
await mount(`
<button>Before</button>
<ix-dropdown-button label="Actions">
<ix-dropdown-item label="Item"></ix-dropdown-item>
</ix-dropdown-button>
<button>After</button>
`);

const dropdownButton = page.locator('ix-dropdown-button');
await expect(dropdownButton).toHaveClass(/\bhydrated\b/);

await page.getByRole('button', { name: 'Before' }).focus();
await page.keyboard.press('Tab');

await expect(dropdownButton).toHaveAttribute('tabindex', '0');
await expect(dropdownButton).toBeFocused();

await page.keyboard.press('Tab');
await expect(page.getByRole('button', { name: 'After' })).toBeFocused();
});

regressionTest('makes nested controls inert', async ({ mount, page }) => {
await mount(
`
<ix-dropdown-button id="labeled-button" label="Actions">
<ix-dropdown-item label="Item"></ix-dropdown-item>
</ix-dropdown-button>
<ix-dropdown-button
id="icon-button"
icon="rocket"
aria-label-dropdown-button="Launch actions"
>
<ix-dropdown-item label="Item"></ix-dropdown-item>
</ix-dropdown-button>
`,
{ icons: { iconRocket } }
);

await expect(
page.locator('#labeled-button').locator('ix-button')
).toHaveAttribute('inert', '');
await expect(
page.locator('#icon-button').locator('ix-icon-button')
).toHaveAttribute('inert', '');
});

regressionTest(
'hides decorative icons from assistive technology',
async ({ mount, page }) => {
await mount(
`
<ix-dropdown-button id="button-with-icons" icon="rocket" label="Actions">
<ix-dropdown-item label="Item"></ix-dropdown-item>
</ix-dropdown-button>
`,
{ icons: { iconRocket } }
);

const decorativeIcons = page
.locator('#button-with-icons')
.locator('.content > ix-icon');
await expect(decorativeIcons).toHaveCount(2);
await expect(decorativeIcons.nth(0)).toHaveAttribute('aria-hidden', 'true');
await expect(decorativeIcons.nth(1)).toHaveAttribute('aria-hidden', 'true');
}
);

regressionTest(
'uses the visible label as the menu accessible name',
async ({ mount, page }) => {
await mount(`
<ix-dropdown-button label="Actions">
<ix-dropdown-item label="Edit"></ix-dropdown-item>
</ix-dropdown-button>
`);

await page.locator('ix-dropdown-button').click();
await expect(page.getByRole('menu', { name: 'Actions' })).toBeVisible();
}
);

regressionTest(
'uses the open state as accessible name when label is empty',
async ({ mount, page }) => {
await mount(`
<ix-dropdown-button label="">
<ix-dropdown-item label="Test"></ix-dropdown-item>
</ix-dropdown-button>
`);

const button = page.locator('ix-dropdown-button');
await expect(button).toHaveAttribute('aria-label', 'Open dropdown');
await expect(button).toHaveAccessibleName('Open dropdown');

await button.click();
await expect(button).toHaveAttribute('aria-label', 'Close dropdown');
await expect(button).toHaveAccessibleName('Close dropdown');
}
);

regressionTest('opens and closes the dropdown', async ({ mount, page }) => {
await mount(`
<ix-dropdown-button label="Open">
<ix-dropdown-item label="Test"></ix-dropdown-item>
</ix-dropdown-button>
`);

await page.locator('ix-dropdown-button').click();
const item = page.locator('ix-dropdown-item');
const item = page.getByRole('menuitem', { name: 'Test' });
await expect(item).toBeVisible();

await item.click();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,34 @@
@use 'misc/common-variables' as vars;
@use 'mixins/shadow-dom/component';

@mixin button-state($state) {
$variants: (
primary,
secondary,
tertiary,
subtle-primary,
subtle-secondary,
subtle-tertiary,
danger-primary,
danger-secondary,
danger-tertiary
);

@each $variant in $variants {
.dropdown-button.#{$variant}:#{$state} {
--theme-btn-#{$variant}--border-color: var(
--theme-btn-#{$variant}--border-color--#{$state}
);
--theme-btn-#{$variant}--background: var(
--theme-btn-#{$variant}--background--#{$state}
);
--theme-btn-#{$variant}--color: var(
--theme-btn-#{$variant}--color--#{$state}
);
}
}
}

@mixin triangle($name: '') {
@if ($name != '') {
$name: '#{$name}-';
Expand Down Expand Up @@ -70,8 +98,12 @@
position: relative;
width: 100%;
height: 100%;
cursor: pointer;
}

@include button-state('hover');
@include button-state('active');

.dropdown-button > ix-button {
width: 100%;
height: 100%;
Expand Down
Loading
Loading