From 8e658f2146c92c0000b1023ebae120856c1921d9 Mon Sep 17 00:00:00 2001 From: Kalle <38327916+Sendouc@users.noreply.github.com> Date: Sat, 11 Apr 2026 07:19:49 +0300 Subject: [PATCH] Lock tier heights when dragging to prevent infinite loop The bug is that when a tier is about to wrap (increase in height), especially on mobile it will loop the drop preview for the lower and upper tier till the page crashes. Example crash page (user report): /tier-list-maker?type=stage-mode&canAddDuplicates=false&state=('Vs!%5BHxJXj4655_sJSj8c42_aJAjd23f_bJBGbfe84d_cJCG5dbb63_wJDG20b2aa_zJFG4169e1W~VItems!%5B%5B'V-zK0O1-SZEwK1k3O5O4q4k5u5PEcK1q1u4u4O3q6O5q6QEbK0k2u2QEaK2O3k6PEsK0u2k6UExK0q3Uv%5D)*('typehstage-mode'~idh.')%2CEvY'V-G'~colorh%23H('idhV-J'~namehK'Y*L1775494063N.*O-SZNP-TCQ-CBU-RMVtierW')%5DY%2C%5B_.Hh!'jGffkPNqQNuUNvW%5DwL330zL553%01zwvuqkjh_YWVUQPONLKJHGE.*_ --- .../components/DraggableItem.module.css | 1 + .../tier-list-maker/components/TierRow.tsx | 52 ++++++++++++++++++- 2 files changed, 52 insertions(+), 1 deletion(-) diff --git a/app/features/tier-list-maker/components/DraggableItem.module.css b/app/features/tier-list-maker/components/DraggableItem.module.css index 63c88faa1..e22f2d388 100644 --- a/app/features/tier-list-maker/components/DraggableItem.module.css +++ b/app/features/tier-list-maker/components/DraggableItem.module.css @@ -1,4 +1,5 @@ .item { + min-height: 50px; cursor: move; touch-action: none; user-select: none; diff --git a/app/features/tier-list-maker/components/TierRow.tsx b/app/features/tier-list-maker/components/TierRow.tsx index 73d25d459..c4cc36b33 100644 --- a/app/features/tier-list-maker/components/TierRow.tsx +++ b/app/features/tier-list-maker/components/TierRow.tsx @@ -5,6 +5,7 @@ import { } from "@dnd-kit/sortable"; import clsx from "clsx"; import { ChevronDown, ChevronUp, Trash } from "lucide-react"; +import { useLayoutEffect, useRef } from "react"; import { Button } from "react-aria-components"; import { useTranslation } from "react-i18next"; import { SendouButton } from "~/components/elements/Button"; @@ -28,6 +29,7 @@ interface TierRowProps { export function TierRow({ tier }: TierRowProps) { const { state, + activeItem, getItemsInTier, handleRemoveTier, handleRenameTier, @@ -44,6 +46,11 @@ export function TierRow({ tier }: TierRowProps) { id: tier.id, }); + const combinedRef = useLockedHeightWhileDragging({ + setNodeRef, + isDragging: activeItem !== null, + }); + const tierIndex = state.tiers.findIndex((t) => t.id === tier.id); const isFirstTier = tierIndex === 0; const isLastTier = tierIndex === state.tiers.length - 1; @@ -121,7 +128,7 @@ export function TierRow({ tier }: TierRowProps) { ) : null}
void; + isDragging: boolean; +}) { + const ref = useRef(null); + + const combinedRef = (node: HTMLDivElement | null) => { + ref.current = node; + setNodeRef(node); + }; + + useLayoutEffect(() => { + const el = ref.current; + if (!el) return; + + if (isDragging) { + const rect = el.getBoundingClientRect(); + const firstItem = el.firstElementChild; + const topOffset = firstItem + ? firstItem.getBoundingClientRect().top - rect.top + : undefined; + + el.style.height = `${rect.height}px`; + el.style.overflow = "hidden"; + + if (topOffset !== undefined) { + el.style.alignContent = "flex-start"; + el.style.paddingTop = `${topOffset}px`; + } + } else { + el.style.height = ""; + el.style.overflow = ""; + el.style.alignContent = ""; + el.style.paddingTop = ""; + } + }, [isDragging]); + + return combinedRef; +} + function tierNameFontSize(name: string) { const length = name.length; for (const breakpoint of TIER_NAME_FONT_SIZE_BREAKPOINTS) {