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
6 changes: 6 additions & 0 deletions .changeset/sharp-pumas-obey.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@db-ux/core-components": patch
---

fix: set DBTabItem internal state `_selected` correctly
- now also sets aria-selected=true|false correctly which improves screen reader behaviour
63 changes: 49 additions & 14 deletions packages/components/src/components/tab-item/tab-item.lite.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import {
onMount,
onUnMount,
onUpdate,
Show,
useDefaultProps,
Expand Down Expand Up @@ -27,6 +28,18 @@ useDefaultProps<DBTabItemProps>({});

export default function DBTabItem(props: DBTabItemProps) {
const _ref = useRef<HTMLInputElement | any>(null);

function setSelectedOnChange(event: any) {
useTarget({
stencil: () => {
state._selected = getBooleanAsString(event.target === _ref);
},
default: () => {
state._selected = event.target === _ref;
}
});
}

// jscpd:ignore-start
const state = useStore<DBTabItemState>({
_selected: false,
Expand All @@ -49,17 +62,16 @@ export default function DBTabItem(props: DBTabItemProps) {
props.onChange(event);
}

// We have different ts types in different frameworks, so we need to use any here
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe we can keep this - the aria snapshots changed so it might be good to keep those additonally


useTarget({
stencil: () => {
const selected = (event.target as any)?.['checked'];
state._selected = getBooleanAsString(selected);
},
default: () => {
state._selected = (event.target as any)?.['checked'];
}
});
if (_ref.checked && !state._selected) {
useTarget({
stencil: () => {
state._selected = getBooleanAsString(true);
},
default: () => {
state._selected = true;
}
});
}

useTarget({
angular: () =>
Expand All @@ -76,12 +88,26 @@ export default function DBTabItem(props: DBTabItemProps) {

onUpdate(() => {
if (state.initialized && _ref) {
useTarget({ react: () => state.handleNameAttribute() });
state.initialized = false;

// deselect this tab when another tab in tablist is selected
_ref.closest('[role=tablist]')?.addEventListener(
'change',
setSelectedOnChange
);

if (props.active) {
useTarget({
stencil: () => {
state._selected = getBooleanAsString(true);
},
default: () => {
state._selected = true;
}
});
_ref.click();
}

useTarget({ react: () => state.handleNameAttribute() });
state.initialized = false;
}
}, [_ref, state.initialized]);

Expand All @@ -91,6 +117,15 @@ export default function DBTabItem(props: DBTabItemProps) {
}
}, [props.name]);

onUnMount(() => {
if (state.initialized && _ref) {
_ref.closest('[role=tablist]')?.removeEventListener(
'change',
setSelectedOnChange
);
}
});

return (
<li class={cls('db-tab-item', props.className)} role="none">
<label
Expand Down
4 changes: 2 additions & 2 deletions packages/components/src/components/tabs/tabs.lite.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -170,11 +170,11 @@ export default function DBTabs(props: DBTabsProps) {
if (tabItem) {
const list = tabItem.parentElement;
if (list) {
const indices = Array.from(list.childNodes).indexOf(
const tabIndex = Array.from(list.children).indexOf(
tabItem
);
if (props.onIndexChange) {
props.onIndexChange(indices);
props.onIndexChange(tabIndex);
}

if (props.onTabSelect) {
Expand Down
17 changes: 7 additions & 10 deletions packages/components/src/components/tabs/tabs.spec.tsx
Original file line number Diff line number Diff line change
@@ -1,21 +1,17 @@
import AxeBuilder from '@axe-core/playwright';
import { expect, test } from '@playwright/experimental-ct-react';

import { existsSync, rmSync, writeFileSync } from 'node:fs';
import { DBTabs } from './index';
// @ts-ignore - vue can only find it with .ts as file ending
import { DEFAULT_VIEWPORT } from '../../shared/constants.ts';
import { DBTabItem } from '../tab-item';
import { DBTabList } from '../tab-list';
import { DBTabPanel } from '../tab-panel';

const filePath = './test-results/onIndexChange.txt';
let tabIndex: number;

const comp: any = (
<DBTabs
onIndexChange={(index: number) =>
writeFileSync(filePath, index.toString())
}>
<DBTabs onIndexChange={(index: number) => (tabIndex = index)}>
<DBTabList>
<DBTabItem data-testid="test">Test 1</DBTabItem>
<DBTabItem data-testid="test2">Test 2</DBTabItem>
Expand Down Expand Up @@ -44,10 +40,11 @@ const testComponent = () => {

const testActions = () => {
test('should be clickable', async ({ mount }) => {
if (existsSync(filePath)) {
rmSync(filePath);
}
expect(tabIndex).toBe(undefined);

// Beware: the comments below actually change the selector for vue
// this is necessary because vue will not trigger a check on an list element but requires the actual
// radio button element, which has the role=tab
const component = await mount(comp);
await component
.getByTestId('test2')
Expand All @@ -59,7 +56,7 @@ const testActions = () => {
.isChecked();
expect(!tabChecked).toBeTruthy();

expect(existsSync(filePath)).toBeTruthy();
expect(tabIndex).toBe(1);
});
};

Expand Down