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 @@ -226,6 +226,41 @@ function getLastDndContextProps(): Record<string, any> {
return call[0];
}

function mockWrapperOwnerDocument() {
const addEventListener = vi.fn();
const removeEventListener = vi.fn();

mocks.useRef.mockImplementationOnce(() => ({
current: {
ownerDocument: {addEventListener, removeEventListener},
},
}));

return {addEventListener, removeEventListener};
}

function runEffectCleanupsOnUnmount(): Array<() => void> {
const cleanups: Array<() => void> = [];

mocks.useEffect.mockImplementation((effect: () => undefined | (() => void)) => {
const cleanup = effect();

if (typeof cleanup === 'function') {
cleanups.push(cleanup);
}
});

return cleanups;
}

function renderDraggableTagInput(overrides: Partial<TagInputProps> = {}): void {
renderTagInput({
values: [ValueTypes.STRING.newValue('alpha'), ValueTypes.STRING.newValue('beta')],
occurrences: Occurrences.minmax(0, 3),
...overrides,
});
}

function getKeyboardSensorOptions(): Record<string, any> {
const call = (mocks.useSensor.mock.calls as unknown as Array<[unknown, Record<string, any>]>).find(
entry => entry[0] === 'KeyboardSensor',
Expand Down Expand Up @@ -554,22 +589,16 @@ describe('TagInput', () => {
});

it('cancels an active drag when scrolling starts', () => {
const addEventListener = vi.fn();
const removeEventListener = vi.fn();
const {addEventListener, removeEventListener} = mockWrapperOwnerDocument();
const setDragContextKey = vi.fn();
const wrapperRef = {current: {ownerDocument: {addEventListener, removeEventListener}}};

mocks.useState
.mockImplementationOnce(() => ['', vi.fn()])
.mockImplementationOnce(() => [false, vi.fn()])
.mockImplementationOnce(() => [false, vi.fn()])
.mockImplementationOnce(() => [0, setDragContextKey]);
mocks.useRef.mockImplementationOnce(() => wrapperRef);

renderTagInput({
values: [ValueTypes.STRING.newValue('alpha'), ValueTypes.STRING.newValue('beta')],
occurrences: Occurrences.minmax(0, 3),
});
renderDraggableTagInput();

const dndContextProps = getLastDndContextProps();
dndContextProps.onDragStart({});
Expand All @@ -583,6 +612,23 @@ describe('TagInput', () => {
expect(removeEventListener).toHaveBeenCalledWith('scroll', scrollHandler, true);
});

it('removes the active drag scroll listener on unmount', () => {
const {addEventListener, removeEventListener} = mockWrapperOwnerDocument();
const effectCleanups = runEffectCleanupsOnUnmount();

renderDraggableTagInput();

const dndContextProps = getLastDndContextProps();
dndContextProps.onDragStart({});

const scrollHandler = addEventListener.mock.calls[0]?.[1];
effectCleanups.forEach(cleanup => {
cleanup();
});

expect(removeEventListener).toHaveBeenCalledWith('scroll', scrollHandler, true);
});

it('moves keyboard drag left and right by logical neighbor order', () => {
renderTagInput({
values: [
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,11 @@ type TagEntry = {
id: string;
};

type DragScrollListener = {
clear: () => void;
listen: (ownerDocument: Document, onScroll: () => void) => void;
};

//
// * Helpers
//
Expand Down Expand Up @@ -237,6 +242,31 @@ function focusElementNextFrame(element: HTMLElement | null | undefined): void {
requestAnimationFrame(() => element?.focus());
}

function clearCleanupRef(cleanupRef: RefObject<(() => void) | null>): void {
cleanupRef.current?.();
cleanupRef.current = null;
}

function useDragScrollListener(): DragScrollListener {
const cleanupRef = useRef<(() => void) | null>(null);

const clear = () => clearCleanupRef(cleanupRef);

useEffect(() => {
return () => clearCleanupRef(cleanupRef);
}, []);

const listen = (ownerDocument: Document, onScroll: () => void) => {
clear();
ownerDocument.addEventListener('scroll', onScroll, true);
cleanupRef.current = () => {
ownerDocument.removeEventListener('scroll', onScroll, true);
};
};

return {clear, listen};
}

function compactHiddenTagSlots(values: Value[], onMove: (fromIndex: number, toIndex: number) => void): void {
let targetIndex = 0;

Expand Down Expand Up @@ -573,8 +603,8 @@ export const TagInput = ({
const skipBlurCommit = useRef(false);
const idsByValue = useRef(new WeakMap<Value, string>());
const nextId = useRef(0);
const scrollListenerCleanupRef = useRef<(() => void) | null>(null);
const isDraggingRef = useRef(false);
const dragScrollListener = useDragScrollListener();
draftRef.current = draft;
const tagEntries = values.reduce<TagEntry[]>((entries, value, index) => {
if (!isRenderableTagValue(value)) {
Expand Down Expand Up @@ -611,13 +641,6 @@ export const TagInput = ({
normalizedDraft,
);

const clearScrollListener = () => {
scrollListenerCleanupRef.current?.();
scrollListenerCleanupRef.current = null;
};

useEffect(() => clearScrollListener, []);

const hasSuppressedHiddenEntries = hiddenErrors.some(
entry => !entry.breaksRequired && entry.validationResults.length === 0,
);
Expand Down Expand Up @@ -871,7 +894,7 @@ export const TagInput = ({

const handleDragEnd = (event: DragEndEvent) => {
isDraggingRef.current = false;
clearScrollListener();
dragScrollListener.clear();

const {active, over} = event;
if (over == null || active.id === over.id) {
Expand All @@ -888,7 +911,7 @@ export const TagInput = ({

const handleDragStart = (_event: DragStartEvent) => {
isDraggingRef.current = true;
clearScrollListener();
dragScrollListener.clear();

const ownerDocument = wrapperRef.current?.ownerDocument;
if (ownerDocument == null) {
Expand All @@ -901,19 +924,16 @@ export const TagInput = ({
}

isDraggingRef.current = false;
clearScrollListener();
dragScrollListener.clear();
setDragContextKey(current => current + 1);
};

ownerDocument.addEventListener('scroll', handleScroll, true);
scrollListenerCleanupRef.current = () => {
ownerDocument.removeEventListener('scroll', handleScroll, true);
};
dragScrollListener.listen(ownerDocument, handleScroll);
};

const handleDragCancel = () => {
isDraggingRef.current = false;
clearScrollListener();
dragScrollListener.clear();
};

return (
Expand Down
Loading