Skip to content

Commit 4857856

Browse files
committed
test: cover pinned scrollbar aria-controls across an unpin/re-pin cycle
The unit test drives the remount directly against PinnedScrollbar. Exercise the same path through DataTable's public API, where unpinning every column drops hasPinnedColumns and re-pinning mounts a fresh scrollbar over the wrapper the previous mount already labelled.
1 parent 8d16c96 commit 4857856

2 files changed

Lines changed: 27 additions & 9 deletions

File tree

‎src/__tests__/pinning.test.tsx‎

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,32 @@ describe('DataTable column pinning', () => {
101101
expect(offsets).toContain(0);
102102
expect(offsets.some(v => v > 0)).toBe(true);
103103
});
104+
105+
test('pinned scrollbar aria-controls still resolves after a column is unpinned and re-pinned', async () => {
106+
const unpinnedCols: TableColumn<Row>[] = columns.map(c => ({ ...c, pinned: undefined }));
107+
const { container, rerender } = render(<DataTable columns={columns} data={data} responsive />);
108+
109+
const wrapper = container.querySelector('.rdt_responsiveWrapper') as HTMLElement;
110+
Object.defineProperty(wrapper, 'scrollWidth', { configurable: true, get: () => 1000 });
111+
Object.defineProperty(wrapper, 'clientWidth', { configurable: true, get: () => 400 });
112+
await act(async () => {
113+
wrapper.dispatchEvent(new Event('scroll'));
114+
});
115+
116+
// Unpinning every column drops hasPinnedColumns, which unmounts the
117+
// scrollbar; re-pinning mounts a fresh one with a new useId, while the
118+
// wrapper keeps the id the first mount stamped on it.
119+
rerender(<DataTable columns={unpinnedCols} data={data} responsive />);
120+
rerender(<DataTable columns={columns} data={data} responsive />);
121+
await act(async () => {
122+
wrapper.dispatchEvent(new Event('scroll'));
123+
});
124+
125+
const thumb = container.querySelector('.rdt_pinnedScrollbarThumb') as HTMLElement;
126+
const controls = thumb.getAttribute('aria-controls');
127+
expect(controls).toBe(wrapper.id);
128+
expect(document.getElementById(controls!)).toBe(wrapper);
129+
});
104130
});
105131

106132
// ── PinnedScrollbar ──────────────────────────────────────────────────────────

‎src/components/PinnedScrollbar.tsx‎

Lines changed: 1 addition & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,6 @@ export default function PinnedScrollbar({
1313
rightInset,
1414
}: PinnedScrollbarProps): JSX.Element | null {
1515
const scrollContainerId = React.useId();
16-
// The id the thumb points at. Only equals scrollContainerId when this
17-
// component is the one that labelled the container — see the sync effect.
18-
const [controlsId, setControlsId] = React.useState(scrollContainerId);
1916
const trackRef = React.useRef<HTMLDivElement>(null);
2017
const thumbRef = React.useRef<HTMLDivElement>(null);
2118
const [thumbWidth, setThumbWidth] = React.useState(0);
@@ -65,11 +62,6 @@ export default function PinnedScrollbar({
6562
if (!el.id) {
6663
el.id = scrollContainerId;
6764
}
68-
// The container keeps whatever id it already had — a host-supplied one, or
69-
// one left behind by a previous mount (useId hands out a fresh value each
70-
// time). Point aria-controls at the id that is actually on the element, or
71-
// it dangles and assistive tech can't resolve the scrollbar's target.
72-
setControlsId(el.id);
7365
el.addEventListener('scroll', update, { passive: true });
7466
const ro = new ResizeObserver(update);
7567
ro.observe(el);
@@ -199,7 +191,7 @@ export default function PinnedScrollbar({
199191
ref={thumbRef}
200192
role="scrollbar"
201193
tabIndex={0}
202-
aria-controls={controlsId}
194+
aria-controls={scrollContainerId}
203195
aria-orientation="horizontal"
204196
aria-valuenow={scrollPercent}
205197
aria-valuemin={0}

0 commit comments

Comments
 (0)