Skip to content
Open
Show file tree
Hide file tree
Changes from all 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