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
49 changes: 49 additions & 0 deletions src/features/home/components/SearchResultGallery.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -61,11 +61,16 @@ describe('SearchResultGallery', () => {

const gallery = container.querySelector<HTMLElement>('.pl-results__gallery');
const cards = Array.from(container.querySelectorAll<HTMLButtonElement>('.pl-results__card'));
const contextBody = container.querySelector<HTMLElement>(
'.pl-results__context .context-sticky-note > p:first-of-type',
);

expect(gallery?.getAttribute('role')).toBe('region');
expect(gallery?.getAttribute('aria-label')).toBe('검색 결과 7곳');
expect(gallery?.tabIndex).toBe(0);
expect(container.querySelectorAll('.pl-results__page')).toHaveLength(2);
expect(contextBody?.style.fontSize).toBe('18px');
expect(contextBody?.style.lineHeight).toBe('22px');
expect(cards.map((card) => card.textContent)).toEqual(
Array.from({ length: 7 }, (_, index) => expect.stringContaining(`장소 ${index + 1}/7`)),
);
Expand Down Expand Up @@ -103,4 +108,48 @@ describe('SearchResultGallery', () => {
expect(nextButton?.disabled).toBe(true);
expect(container.querySelector('.pl-results__page-status')?.textContent).toBe('2/2');
});

it('결과 영역을 좌우로 드래그하면 가장 가까운 6개 단위 페이지로 이동한다', () => {
const onSelectRecord = vi.fn();
act(() => {
root.render(<SearchResultGallery items={ITEMS} onSelectRecord={onSelectRecord} />);
});

const gallery = container.querySelector<HTMLElement>('.pl-results__gallery')!;
const pages = Array.from(container.querySelectorAll<HTMLElement>('.pl-results__page'));
const firstCard = container.querySelector<HTMLButtonElement>('.pl-results__card')!;
const scrollTo = vi.fn();

Object.defineProperties(gallery, {
scrollLeft: { configurable: true, writable: true, value: 0 },
scrollTo: { configurable: true, value: scrollTo },
setPointerCapture: { configurable: true, value: vi.fn() },
hasPointerCapture: { configurable: true, value: () => true },
releasePointerCapture: { configurable: true, value: vi.fn() },
});
Object.defineProperty(pages[1], 'offsetLeft', { configurable: true, value: 960 });

act(() => {
gallery.dispatchEvent(
Object.assign(new MouseEvent('pointerdown', { bubbles: true, clientX: 800, button: 0 }), {
pointerId: 1,
}),
);
gallery.dispatchEvent(
Object.assign(new MouseEvent('pointermove', { bubbles: true, clientX: 200 }), {
pointerId: 1,
}),
);
gallery.dispatchEvent(
Object.assign(new MouseEvent('pointerup', { bubbles: true, clientX: 200 }), {
pointerId: 1,
}),
);
firstCard.click();
});

expect(gallery.scrollLeft).toBe(600);
expect(scrollTo).toHaveBeenCalledWith({ left: 960, behavior: 'smooth' });
expect(onSelectRecord).not.toHaveBeenCalled();
});
});
76 changes: 73 additions & 3 deletions src/features/home/components/SearchResultGallery.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,10 @@ interface SearchResultGalleryProps {
}

const SEARCH_RESULT_PAGE_SIZE = 6;
const DRAG_THRESHOLD_PX = 6;
const SEARCH_NOTE_METRICS = {
bodyFontPx: 15,
bodyLineHeightPx: 18,
bodyFontPx: 18,
bodyLineHeightPx: 22,
metaFontPx: 9,
padXPx: 14,
padTopPx: 18,
Expand Down Expand Up @@ -52,7 +53,12 @@ export function SearchResultGallery({ items, onSelectRecord }: SearchResultGalle
);
const galleryId = useId();
const galleryRef = useRef<HTMLDivElement>(null);
const dragStartXRef = useRef(0);
const dragStartScrollLeftRef = useRef(0);
const didDragRef = useRef(false);
const isDraggingRef = useRef(false);
const [pageIndex, setPageIndex] = useState(0);
const [isDragging, setIsDragging] = useState(false);

const scrollToPage = (nextPageIndex: number) => {
const gallery = galleryRef.current;
Expand Down Expand Up @@ -89,16 +95,80 @@ export function SearchResultGallery({ items, onSelectRecord }: SearchResultGalle
setPageIndex(nearestPageIndex);
};

const handlePointerDown = (event: React.PointerEvent<HTMLDivElement>) => {
if (event.button !== 0) {
return;
}
const gallery = galleryRef.current;
if (gallery === null) {
return;
}
dragStartXRef.current = event.clientX;
dragStartScrollLeftRef.current = gallery.scrollLeft;
didDragRef.current = false;
isDraggingRef.current = true;
setIsDragging(true);
gallery.setPointerCapture(event.pointerId);
};

const handlePointerMove = (event: React.PointerEvent<HTMLDivElement>) => {
const gallery = galleryRef.current;
if (!isDraggingRef.current || gallery === null) {
return;
}
const deltaX = event.clientX - dragStartXRef.current;
if (Math.abs(deltaX) >= DRAG_THRESHOLD_PX) {
didDragRef.current = true;
}
if (didDragRef.current) {
gallery.scrollLeft = dragStartScrollLeftRef.current - deltaX;
}
};

const finishDrag = (event: React.PointerEvent<HTMLDivElement>) => {
const gallery = galleryRef.current;
if (!isDraggingRef.current || gallery === null) {
return;
}
isDraggingRef.current = false;
setIsDragging(false);
if (gallery.hasPointerCapture(event.pointerId)) {
gallery.releasePointerCapture(event.pointerId);
}
if (didDragRef.current) {
const pages = Array.from(gallery.querySelectorAll<HTMLElement>('.pl-results__page'));
const nearestPageIndex = pages.reduce(
(nearest, page, index) =>
Math.abs(gallery.scrollLeft - page.offsetLeft) <
Math.abs(gallery.scrollLeft - pages[nearest].offsetLeft)
? index
: nearest,
0,
);
scrollToPage(nearestPageIndex);
}
};

return (
<div className="pl-results__viewport">
<div
ref={galleryRef}
id={galleryId}
className="pl-results__gallery"
className={`pl-results__gallery${isDragging ? ' pl-results__gallery--dragging' : ''}`}
role="region"
aria-label={`검색 결과 ${total}곳`}
tabIndex={0}
onScroll={handleGalleryScroll}
onPointerDown={handlePointerDown}
onPointerMove={handlePointerMove}
onPointerUp={finishDrag}
onPointerCancel={finishDrag}
onClickCapture={(event) => {
if (didDragRef.current) {
event.preventDefault();
event.stopPropagation();
}
}}
>
{pages.map((pageItems, pageNumber) => (
<div
Expand Down
8 changes: 8 additions & 0 deletions src/features/home/paperAperture.css
Original file line number Diff line number Diff line change
Expand Up @@ -1119,9 +1119,17 @@
scroll-padding-inline: 12px;
scroll-snap-type: x mandatory;
scrollbar-color: theme('colors.log-mint') rgba(250, 247, 246, 0.82);
cursor: grab;
touch-action: pan-y;
outline: none;
}

.pl-results__gallery--dragging {
cursor: grabbing;
user-select: none;
scroll-snap-type: none;
}

.pl-results__gallery:focus-visible {
border-radius: 18px;
outline: 2px solid theme('colors.log-mint');
Expand Down
Loading