diff --git a/.changeset/friendly-card-list-focus.md b/.changeset/friendly-card-list-focus.md new file mode 100644 index 00000000000..98604e2f1be --- /dev/null +++ b/.changeset/friendly-card-list-focus.md @@ -0,0 +1,5 @@ +--- +'@siemens/ix': patch +--- + +Prevent focus from remaining inside collapsed card lists. \ No newline at end of file diff --git a/packages/core/src/components/card-list/card-list.tsx b/packages/core/src/components/card-list/card-list.tsx index 9bd1f40f0b6..0c6caf4d814 100644 --- a/packages/core/src/components/card-list/card-list.tsx +++ b/packages/core/src/components/card-list/card-list.tsx @@ -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'; @@ -25,6 +26,7 @@ function CardListTitle(props: { labelShowLess: string; showLess: boolean; hideShowAll: boolean; + collapseButtonRef: (element?: HTMLIxIconButtonElement) => void; }) { if (!props.label) { return null; @@ -42,6 +44,7 @@ function CardListTitle(props: { CardList__Title__Button__Collapsed: props.isCollapsed, }} aria-label={props.ariaLabelExpandButton} + ref={props.collapseButtonRef} > {props.label} @@ -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, @@ -337,6 +363,7 @@ export class CardList { this.onCardListVisibilityToggle()} onShowAllClick={(e) => this.onShowAllClick(e)} hideShowAll={this.hideShowAll} + collapseButtonRef={(element) => (this.collapseButton = element)} >
this.onCardListScroll()} + inert={this.collapse} > { diff --git a/packages/core/src/components/card-list/test/card-list.ct.ts b/packages/core/src/components/card-list/test/card-list.ct.ts index 586163bca94..3cbfe20da86 100644 --- a/packages/core/src/components/card-list/test/card-list.ct.ts +++ b/packages/core/src/components/card-list/test/card-list.ct.ts @@ -18,6 +18,50 @@ const CARDS_HTML = ` Card 5 `; +regressionTest( + 'prevents focus from remaining in collapsed card content', + async ({ mount, page }) => { + await mount(` + + + + + + + + + + `); + + 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 }) => {