Skip to content
Draft
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/friendly-card-list-focus.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@siemens/ix': patch
---

Prevent focus from remaining inside collapsed card lists.
29 changes: 29 additions & 0 deletions packages/core/src/components/card-list/card-list.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
Listen,
Prop,
State,
Watch,
} from '@stencil/core';
import { createMutationObserver } from '../utils/mutation-observer';
import { iconChevronUp, iconMoreMenu } from '@siemens/ix-icons/icons';
Expand All @@ -25,6 +26,7 @@ function CardListTitle(props: {
labelShowLess: string;
showLess: boolean;
hideShowAll: boolean;
collapseButtonRef: (element?: HTMLIxIconButtonElement) => void;
}) {
if (!props.label) {
return null;
Expand All @@ -42,6 +44,7 @@ function CardListTitle(props: {
CardList__Title__Button__Collapsed: props.isCollapsed,
}}
aria-label={props.ariaLabelExpandButton}
ref={props.collapseButtonRef}
></ix-icon-button>
<ix-typography class="CardList_Title__Label" format="body-lg">
{props.label}
Expand Down Expand Up @@ -178,11 +181,34 @@ export class CardList {

private observer?: MutationObserver;

private collapseButton?: HTMLIxIconButtonElement;

private onCardListVisibilityToggle() {
this.collapse = !this.collapse;
this.collapseChanged.emit(this.collapse);
}

@Watch('collapse')
protected handleCollapseChange(isCollapsed: boolean) {
if (isCollapsed && this.hasFocusWithinListContent()) {
this.collapseButton?.focus();
}
}

private hasFocusWithinListContent() {
const activeElement = document.activeElement;

if (!activeElement) {
return false;
}

return this.getListChildren().some(
(child) =>
child === activeElement ||
(child instanceof HTMLElement && child.contains(activeElement))
);
}

private handleClick(emitter: EventEmitter, event: MouseEvent) {
const { defaultPrevented } = emitter.emit({
nativeEvent: event,
Expand Down Expand Up @@ -337,6 +363,7 @@ export class CardList {
<CardListTitle
isCollapsed={this.collapse}
label={this.label}
ariaLabelExpandButton={this.ariaLabelExpandButton}
showAllLabel={this.i18nShowAll}
showAllCounter={
this.showAllCount === undefined
Expand All @@ -348,6 +375,7 @@ export class CardList {
onClick={() => this.onCardListVisibilityToggle()}
onShowAllClick={(e) => this.onShowAllClick(e)}
hideShowAll={this.hideShowAll}
collapseButtonRef={(element) => (this.collapseButton = element)}
></CardListTitle>
<div
class={{
Expand All @@ -363,6 +391,7 @@ export class CardList {
CardList__Style__Infinite__Scroll: this.listStyle === 'scroll',
}}
onScroll={() => this.onCardListScroll()}
inert={this.collapse}
>
<slot
onSlotchange={() => {
Expand Down
44 changes: 44 additions & 0 deletions packages/core/src/components/card-list/test/card-list.ct.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,50 @@ const CARDS_HTML = `
<ix-card><ix-card-content>Card 5</ix-card-content></ix-card>
`;

regressionTest(
'prevents focus from remaining in collapsed card content',
async ({ mount, page }) => {
await mount(`
<button id="before">Before</button>
<ix-card-list
label="Test"
aria-label-expand-button="Toggle card list"
hide-show-all
>
<ix-card>
<ix-card-content>
<button id="card-action">Card action</button>
</ix-card-content>
</ix-card>
</ix-card-list>
<button id="after">After</button>
`);

const cardList = page.locator('ix-card-list');
const collapseButton = cardList.getByRole('button', {
name: 'Toggle card list',
});
const cardAction = page.locator('#card-action');
const content = cardList.locator('.CardList__Content');
const after = page.locator('#after');

await expect(cardList).toHaveClass(/\bhydrated\b/);

await cardAction.focus();
await expect(cardAction).toBeFocused();

await cardList.evaluate((element: HTMLIxCardListElement) => {
element.collapse = true;
});

await expect(collapseButton).toBeFocused();
await expect(content).toHaveJSProperty('inert', true);

await page.keyboard.press('Tab');
await expect(after).toBeFocused();
}
);

regressionTest(
'show all button reveals all hidden cards',
async ({ mount, page }) => {
Expand Down
Loading