Skip to content

Commit 668a64c

Browse files
committed
Merge branch 'feature/3720_select-overflow-stable' of https://github.com/siemens/ix into feature/3720_select-overflow-stable
2 parents eed31f4 + 84a3c85 commit 668a64c

13 files changed

Lines changed: 201 additions & 133 deletions

File tree

.changeset/fast-jolly-safe.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@siemens/ix': patch
3+
---
4+
5+
Fix `ix-input` password eye icon: remove debug `color: red` style and conditionally render the toggle button instead of hiding it via CSS class.

.changeset/pink-sloths-drop.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
'@siemens/ix': patch
3+
---
4+
5+
Fix **ix-action-card** to render its content inside a `button` element so it is keyboard-focusable and exposed as a button to assistive technologies, improving accessibility.
6+
7+
Note: the default slot is intended for non-interactive content. Nested interactive elements (links, buttons, inputs) inside the card are not supported, as the card is now a single button.

packages/angular-standalone-test-app/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@
2929
"ag-grid-angular": "^33.1.1",
3030
"ag-grid-community": "^33.1.1",
3131
"echarts": "^6.0.0",
32-
"echarts-gl": "^2.0.9",
32+
"echarts-gl": "^2.1.0",
3333
"html-test-app": "workspace:*",
3434
"ngx-echarts": "^20.0.1",
3535
"rxjs": "~7.8.0",

packages/angular-test-app/package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,8 @@
3030
"@siemens/ix-icons": "catalog:",
3131
"ag-grid-angular": "^33.1.1",
3232
"ag-grid-community": "^33.1.1",
33-
"echarts": "5.4.3",
34-
"echarts-gl": "^2.0.9",
33+
"echarts": "^6.0.0",
34+
"echarts-gl": "^2.1.0",
3535
"html-test-app": "workspace:*",
3636
"ngx-echarts": "^20.0.1",
3737
"rxjs": "~7.8.0",

packages/core/src/components/action-card/action-card.scss

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,31 @@
2121

2222
@include component.ix-component;
2323

24+
25+
button {
26+
width: 100%;
27+
height: 100%;
28+
display: block;
29+
background: transparent;
30+
text-align: start;
31+
padding: 0;
32+
border: 0;
33+
border-radius: var(--theme-btn--border-radius);
34+
box-shadow: initial;
35+
36+
&:focus-visible {
37+
outline: 1px solid var(--theme-color-focus-bdr);
38+
outline-offset: var(--theme-btn--focus--outline-offset);
39+
}
40+
41+
&[disabled] {
42+
cursor: default;
43+
}
44+
}
45+
2446
ix-card {
2547
width: 100%;
2648
height: 100%;
2749
}
2850
}
51+

packages/core/src/components/action-card/action-card.tsx

Lines changed: 47 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,12 @@
88
*/
99

1010
import { Component, h, Host, Prop } from '@stencil/core';
11-
import type { ActionCardVariant } from './action-card.types';
1211
import { a11yBoolean, getFallbackLabelFromIconName } from '../utils/a11y';
12+
import type { ActionCardVariant } from './action-card.types';
1313

14+
/**
15+
* @slot - Place additional non-interactive content inside the card. Avoid interactive elements (links, buttons, inputs) as the card itself is rendered as a single button.
16+
*/
1417
@Component({
1518
tag: 'ix-action-card',
1619
styleUrl: 'action-card.scss',
@@ -75,48 +78,53 @@ export class IxActionCard {
7578

7679
return (
7780
<Host>
78-
<ix-card
79-
selected={this.selected}
80-
variant={this.variant}
81-
passive={this.passive}
82-
class={'pointer'}
81+
<button
82+
type="button"
83+
disabled={this.passive}
8384
aria-label={this.ariaLabelCard}
8485
aria-labelledby={ariaLabelledBy}
85-
role={ariaLabelledBy ? 'group' : undefined}
8686
>
87-
<ix-card-content>
88-
{this.icon ? (
89-
<ix-icon
90-
class={'icon'}
91-
name={this.icon}
92-
size="32"
93-
aria-label={
94-
this.ariaLabelIcon || getFallbackLabelFromIconName(this.icon)
95-
}
96-
></ix-icon>
97-
) : null}
98-
<div>
99-
{this.heading ? (
100-
<ix-typography
101-
id="ix-action-card-heading"
102-
aria-hidden={a11yBoolean(!ariaLabelledBy)}
103-
format="h4"
104-
>
105-
{this.heading}
106-
</ix-typography>
107-
) : null}
108-
{this.subheading ? (
109-
<ix-typography
110-
format="h5"
111-
text-color={this.getSubheadingTextColor()}
112-
>
113-
{this.subheading}
114-
</ix-typography>
87+
<ix-card
88+
selected={this.selected}
89+
variant={this.variant}
90+
passive={this.passive}
91+
class={this.passive ? undefined : 'pointer'}
92+
>
93+
<ix-card-content>
94+
{this.icon ? (
95+
<ix-icon
96+
class={'icon'}
97+
name={this.icon}
98+
size="32"
99+
aria-label={
100+
this.ariaLabelIcon ||
101+
getFallbackLabelFromIconName(this.icon)
102+
}
103+
></ix-icon>
115104
) : null}
116-
<slot></slot>
117-
</div>
118-
</ix-card-content>
119-
</ix-card>
105+
<div>
106+
{this.heading ? (
107+
<ix-typography
108+
id="ix-action-card-heading"
109+
aria-hidden={a11yBoolean(!ariaLabelledBy)}
110+
format="h4"
111+
>
112+
{this.heading}
113+
</ix-typography>
114+
) : null}
115+
{this.subheading ? (
116+
<ix-typography
117+
format="h5"
118+
text-color={this.getSubheadingTextColor()}
119+
>
120+
{this.subheading}
121+
</ix-typography>
122+
) : null}
123+
<slot></slot>
124+
</div>
125+
</ix-card-content>
126+
</ix-card>
127+
</button>
120128
</Host>
121129
);
122130
}
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
/*
2+
* SPDX-FileCopyrightText: 2025 Siemens AG
3+
*
4+
* SPDX-License-Identifier: MIT
5+
*
6+
* This source code is licensed under the MIT license found in the
7+
* LICENSE file in the root directory of this source tree.
8+
*/
9+
import { expect } from '@playwright/test';
10+
import { regressionTest } from '@utils/test';
11+
12+
regressionTest('renders', async ({ mount, page }) => {
13+
await mount(`
14+
<ix-action-card heading="Heading" subheading="Subheading"></ix-action-card>
15+
`);
16+
const actionCard = page.locator('ix-action-card');
17+
await expect(actionCard).toHaveClass(/\bhydrated\b/);
18+
await expect(actionCard.locator('button')).toBeVisible();
19+
});
20+
21+
regressionTest(
22+
'passive card renders a disabled button',
23+
async ({ mount, page }) => {
24+
await mount(`
25+
<ix-action-card heading="Heading" subheading="Subheading" passive></ix-action-card>
26+
`);
27+
const actionCard = page.locator('ix-action-card');
28+
await expect(actionCard).toHaveClass(/\bhydrated\b/);
29+
await expect(actionCard.locator('button')).toBeDisabled();
30+
}
31+
);
32+
33+
regressionTest.describe('accessibility', () => {
34+
regressionTest('default', async ({ mount, makeAxeBuilder }) => {
35+
await mount(`
36+
<ix-action-card heading="Heading" subheading="Subheading"></ix-action-card>
37+
`);
38+
39+
const accessibilityScanResults = await makeAxeBuilder().analyze();
40+
expect(accessibilityScanResults.violations).toEqual([]);
41+
});
42+
43+
regressionTest('passive', async ({ mount, makeAxeBuilder }) => {
44+
await mount(`
45+
<ix-action-card heading="Heading" subheading="Subheading" passive></ix-action-card>
46+
`);
47+
48+
const accessibilityScanResults = await makeAxeBuilder().analyze();
49+
expect(accessibilityScanResults.violations).toEqual([]);
50+
});
51+
});

packages/core/src/components/input/input.scss

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,6 @@
2727
}
2828

2929
.password-eye {
30-
&.eye-hidden {
31-
display: none;
32-
}
30+
margin-left: 0.125rem;
3331
}
3432
}

packages/core/src/components/input/input.tsx

Lines changed: 20 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -194,6 +194,7 @@ export class Input implements IxInputFieldComponent<string> {
194194
private readonly inputRef = makeRef<HTMLInputElement>();
195195
private readonly slotEndRef = makeRef<HTMLDivElement>();
196196
private readonly slotStartRef = makeRef<HTMLDivElement>();
197+
private readonly passwordToggleRef = () => this.updatePaddings();
197198
private readonly inputId = `input-${inputIds++}`;
198199
private touched = false;
199200

@@ -353,26 +354,26 @@ export class Input implements IxInputFieldComponent<string> {
353354
slotEndRef={this.slotEndRef}
354355
onSlotChange={() => this.updatePaddings()}
355356
>
356-
<ix-icon-button
357-
color="color-weak-text"
358-
class={{
359-
'password-eye': true,
360-
'eye-hidden': this.type !== 'password' || this.disabled,
361-
}}
362-
variant="tertiary"
363-
size="16"
364-
icon={
365-
this.inputType === 'password' ? iconEye : iconEyeCancelled
366-
}
367-
onClick={() => {
368-
if (this.inputType === 'password') {
369-
this.inputType = 'text';
370-
return;
357+
{this.type === 'password' && !this.disabled && (
358+
<ix-icon-button
359+
ref={this.passwordToggleRef}
360+
color="color-weak-text"
361+
class="password-eye"
362+
variant="tertiary"
363+
size="16"
364+
icon={
365+
this.inputType === 'password' ? iconEye : iconEyeCancelled
371366
}
372-
373-
this.inputType = 'password';
374-
}}
375-
></ix-icon-button>
367+
onClick={() => {
368+
if (this.inputType === 'password') {
369+
this.inputType = 'text';
370+
return;
371+
}
372+
373+
this.inputType = 'password';
374+
}}
375+
></ix-icon-button>
376+
)}
376377
</SlotEnd>
377378
</div>
378379
{!!this.maxLength && this.maxLength > 0 && (

packages/core/src/components/input/tests/password-input.ct.ts

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,16 @@ test.describe('password input', () => {
1313
let input: import('@playwright/test').Locator;
1414
let eyeButton: import('@playwright/test').Locator;
1515

16+
test('accessibility', async ({ mount, makeAxeBuilder }) => {
17+
await mount(`
18+
<ix-input type="password" value="secret123"></ix-input>
19+
<ix-input type="password" disabled value="secret123"></ix-input>
20+
`);
21+
22+
const results = await makeAxeBuilder().analyze();
23+
expect(results.violations).toEqual([]);
24+
});
25+
1626
test.beforeEach(async ({ page }) => {
1727
input = page.locator('ix-input');
1828
eyeButton = input.locator('ix-icon-button.password-eye');
@@ -21,7 +31,7 @@ test.describe('password input', () => {
2131
test.describe('eye icon visibility', () => {
2232
test('eye icon is visible for password type', async ({ mount }) => {
2333
await mount('<ix-input type="password" value="secret123"></ix-input>');
24-
await expect(eyeButton).not.toHaveClass(/eye-hidden/);
34+
await expect(eyeButton).toBeAttached();
2535
await expect(eyeButton).toBeVisible();
2636
});
2737

@@ -35,8 +45,7 @@ test.describe('password input', () => {
3545
for (const { type, value } of nonPasswordInputTypes) {
3646
test(`eye icon is hidden for ${type} type`, async ({ mount }) => {
3747
await mount(`<ix-input type="${type}" value="${value}"></ix-input>`);
38-
await expect(eyeButton).toHaveClass(/eye-hidden/);
39-
await expect(eyeButton).not.toBeVisible();
48+
await expect(eyeButton).not.toBeAttached();
4049
});
4150
}
4251

@@ -46,8 +55,7 @@ test.describe('password input', () => {
4655
await mount(
4756
'<ix-input type="password" disabled value="secret"></ix-input>'
4857
);
49-
await expect(eyeButton).toHaveClass(/eye-hidden/);
50-
await expect(eyeButton).not.toBeVisible();
58+
await expect(eyeButton).not.toBeAttached();
5159
});
5260

5361
test('eye icon is visible when password input is readonly', async ({
@@ -56,7 +64,7 @@ test.describe('password input', () => {
5664
await mount(
5765
'<ix-input type="password" readonly value="secret"></ix-input>'
5866
);
59-
await expect(eyeButton).not.toHaveClass(/eye-hidden/);
67+
await expect(eyeButton).toBeAttached();
6068
await expect(eyeButton).toBeVisible();
6169
});
6270
});

0 commit comments

Comments
 (0)