Skip to content
Merged
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
Original file line number Diff line number Diff line change
Expand Up @@ -326,6 +326,88 @@ describe('task modal log window', () => {
expect(list.querySelectorAll('[data-index]').length).toBeLessThan(40);
});

it.each(['blurred', 'focused'] as const)(
'retains the same %s offscreen editor when the discussion crosses the window threshold',
async (focus) => {
const comments = state.comments.slice(0, 120);
state.comments = comments.slice(0, 90);
const view = render(<Harness mode="comments" canComment />);
const scroller = screen.getByTestId('scrollport');
const list = scroller.querySelector('ul');
if (list === null) throw new Error('Missing comment list');
expect(list.querySelectorAll('[data-index]')).toHaveLength(90);
await scrollTo(scroller, listStart(list, scroller));
const comment = await screen.findByText('Comment 0 opening line.');
const row = comment.closest('li');
if (row === null) throw new Error('Missing comment row');
await userEvent.click(
within(row).getByRole('button', { name: 'actions.edit' }),
);
const field = within(row).getByRole('textbox');
const draft = 'Draft retained across windowing';
await userEvent.fill(field, draft);
expect(field).toHaveFocus();
if (focus === 'blurred') {
await userEvent.click(
screen.getByRole('button', { name: 'detail.showEarlierComments' }),
);
expect(field).not.toHaveFocus();
expect(row.contains(document.activeElement)).toBe(false);
}
await scrollTo(
scroller,
listStart(list, scroller) + list.offsetHeight - scroller.clientHeight,
);
await waitFor(() => {
expect(screen.getByText('Comment 89 opening line.')).toBeVisible();
expect(row.getBoundingClientRect().bottom).toBeLessThan(
scroller.getBoundingClientRect().top,
);
});
// Allow the shared focus bridge's deferred blur release to finish before
// crossing. Otherwise its focus pin could mask missing edit registration.
await act(async () => {
for (let frame = 0; frame < 3; frame++) {
await new Promise<void>((resolve) =>
requestAnimationFrame(() => resolve()),
);
}
});
if (focus === 'focused') expect(field).toHaveFocus();
else expect(row.contains(document.activeElement)).toBe(false);
state.comments = comments;
view.rerender(<Harness mode="comments" canComment />);
await waitFor(() => {
expect(list.querySelectorAll('[data-index]').length).toBeGreaterThan(0);
expect(list.querySelectorAll('[data-index]').length).toBeLessThan(40);
// A nearby unpinned row must leave the range as well: bounded rows
// alone would not prove that the edit is outside the visible window.
expect(screen.queryByText('Comment 10 opening line.')).toBeNull();
expect(row.getBoundingClientRect().bottom).toBeLessThan(
scroller.getBoundingClientRect().top,
);
});
await act(async () => {
for (let frame = 0; frame < 3; frame++) {
await new Promise<void>((resolve) =>
requestAnimationFrame(() => resolve()),
);
}
});
expect(list.querySelectorAll('[data-index]').length).toBeLessThan(40);
expect(field).toBeInTheDocument();
expect(within(row).getByRole('textbox')).toBe(field);
expect(field).toHaveValue(draft);
if (focus === 'focused') {
expect(field).toHaveFocus();
expect(document.activeElement).not.toBe(document.body);
} else {
expect(field).not.toHaveFocus();
expect(row.contains(document.activeElement)).toBe(false);
}
},
);

it('keeps an edit draft mounted after scrolling into older pages and moving focus', async () => {
render(<Harness mode="comments" canComment leadHeight={0} />);
const scroller = screen.getByTestId('scrollport');
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@ const TaskLogViewportContext =

type RowKey = string | number;
type SetRowActive = (rowKey: RowKey, source: string, active: boolean) => void;
const NOOP_SET_ROW_ACTIVE: SetRowActive = () => undefined;
const TaskLogRowActivityContext = createContext<
((source: string, active: boolean) => void) | null
>(null);
Expand Down Expand Up @@ -170,12 +169,12 @@ export function useTaskLogWindow({
return () => observer.disconnect();
}, [getScrollElement, window.scrollElement, listElement]);

// Small discussions render every row and do not need an activity pin. A
// no-op keeps editor focus from causing a second list reconciliation in that
// common case; large windows still pin dirty or portaled rows as they leave
// the viewport.
// Register activity in every mode. A row can become active while the list is
// below the window threshold and remain active when new rows turn
// virtualization on; keeping the registration avoids dropping its draft on
// that crossing render.
return Object.assign(window, {
listRef: attachList,
setRowActive: window.virtualized ? setRowActive : NOOP_SET_ROW_ACTIVE,
setRowActive,
});
}
Loading