05행 삭제 값과 높이 값 동시 변화 — 행의 존재 값이 사라지면 그 행의 높이 값이 0으로 줄며 아래 행들의 위치 값이 빈자리를 채우는 방식
행의 존재 값이 사라지는 순간 그 행의 높이 값이 0으로 줄어들고, 아래 있던 행들의 위치 값이 그 빈자리로 옮겨와 채웁니다.
쓰임항목을 지울 수 있는 관리자 표, 완료 후 목록에서 사라지는 작업 표
POS_LERP 0.22H_LERP 0.26.tdc__inner

<main class="stage">
<div class="tdc is-demo" id="tdc" role="table" aria-label="삭제 가능한 작업 표"></div>
</main>@use "../../../../site/static/demo-tokens" as *;
// 계산 겹: 삭제 값이 켜지면 그 행의 높이 값이 0으로 줄고(H_LERP), 아래 행들의 순서(슬롯) 값이
// 하나씩 당겨져 목표 y 가 다시 계산되어 위치 값이 보간(POS_LERP)으로 채운다 — index.html remove()/tick() 참조
@include stage($stage-ink);
.tdc {
position: relative;
width: 100%;
max-width: 240px;
overflow: hidden;
}
.tdc__row {
position: absolute;
top: 0;
left: 0;
width: 100%;
}
.tdc__inner {
overflow: hidden;
box-sizing: border-box;
height: 34px;
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 $sp-3;
background: rgba(255, 247, 230, .06);
border-radius: $r-sm;
color: $subject-cream;
font: 400 12px/1 -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
}
.tdc__x {
border: 0;
background: transparent;
color: $error-soft;
font-size: 16px;
line-height: 1;
cursor: pointer;
padding: $sp-1;
}
@media (max-width: 340px) {
.tdc { max-width: 200px; }
.tdc__inner { height: 30px; font-size: 11px; }
}
@media (prefers-reduced-motion: reduce) {
.tdc__row { transition: none; }
}html, body {
margin: 0;
overflow: hidden;
}
.stage {
box-sizing: border-box;
min-height: 100vh;
display: grid;
place-items: center;
margin: 0;
background: #17141a;
font-family: -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
padding: clamp(8px, 4vw, 24px);
}
.tdc {
position: relative;
width: 100%;
max-width: 240px;
overflow: hidden;
}
.tdc__row {
position: absolute;
top: 0;
left: 0;
width: 100%;
}
.tdc__inner {
overflow: hidden;
box-sizing: border-box;
height: 34px;
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 12px;
background: rgba(255, 247, 230, 0.06);
border-radius: 8px;
color: #fff7e6;
font: 400 12px/1 -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
}
.tdc__x {
border: 0;
background: transparent;
color: #ffe0d8;
font-size: 16px;
line-height: 1;
cursor: pointer;
padding: 4px;
}
@media (max-width: 340px) {
.tdc {
max-width: 200px;
}
.tdc__inner {
height: 30px;
font-size: 11px;
}
}
@media (prefers-reduced-motion: reduce) {
.tdc__row {
transition: none;
}
}(function () {
var root = document.getElementById('tdc');
// 상수: 행 높이(px) · 위치 보간 속도 · 높이 보간 속도 · 슬롯 수 · 자동 삭제 간격
var ROW_H = 34;
var POS_LERP = 0.22;
var H_LERP = 0.26;
var SLOTS = 4;
var AUTO_DELETE_MS = 1300;
var seq = 0;
var rows = []; // {el, inner, order, y, h, removing, entering}
function makeRow(label) {
var el = document.createElement('div');
el.className = 'tdc__row';
var inner = document.createElement('div');
inner.className = 'tdc__inner';
var cell = document.createElement('span');
cell.className = 'tdc__cell';
cell.textContent = '작업 ' + label;
var btn = document.createElement('button');
btn.className = 'tdc__x';
btn.type = 'button';
btn.setAttribute('aria-label', '삭제');
btn.textContent = '×';
inner.appendChild(cell);
inner.appendChild(btn);
el.appendChild(inner);
root.appendChild(el);
return { el: el, inner: inner };
}
function addRow(entering) {
seq++;
var made = makeRow(seq);
var order = rows.filter(function (r) { return !r.removing; }).length;
var entry = { el: made.el, inner: made.inner, order: order, y: order * ROW_H, h: entering ? 0 : ROW_H, removing: false, entering: !!entering };
made.inner.querySelector('.tdc__x').addEventListener('click', function (e) {
e.stopPropagation();
root.classList.remove('is-demo');
remove(entry);
});
rows.push(entry);
return entry;
}
// 계산 겹: 삭제 값이 켜지면 그 행의 높이 값이 0으로 줄고, 아래 행들의 순서(슬롯) 값이 하나씩 당겨져
// 목표 y 가 다시 계산된다
function remove(entry) {
if (entry.removing) return;
entry.removing = true;
rows.forEach(function (r) {
if (!r.removing && r.order > entry.order) r.order -= 1;
});
}
function tick() {
for (var i = rows.length - 1; i >= 0; i--) {
var r = rows[i];
var targetY = r.order * ROW_H;
r.y += (targetY - r.y) * POS_LERP;
var targetH = r.removing ? 0 : ROW_H;
r.h += (targetH - r.h) * H_LERP;
r.el.style.transform = 'translateY(' + r.y.toFixed(1) + 'px)';
r.inner.style.height = Math.max(0, r.h).toFixed(1) + 'px';
r.inner.style.opacity = (r.h / ROW_H).toFixed(2);
if (r.removing && r.h < 0.5) {
r.el.remove();
rows.splice(i, 1);
}
}
requestAnimationFrame(tick);
}
requestAnimationFrame(tick);
addRow(false); addRow(false); addRow(false); addRow(false);
root.style.height = (SLOTS * ROW_H) + 'px';
// 자동 루프: 맨 위 행을 지우고 새 행을 아래에 채운다
function autoStep() {
if (root.classList.contains('is-demo') && rows.length) {
remove(rows[0]);
addRow(true);
}
setTimeout(autoStep, AUTO_DELETE_MS);
}
setTimeout(autoStep, AUTO_DELETE_MS);
})();import { useEffect, useRef } from "react";
import styles from "./TableRowDeleteCollapse.module.scss";
type Box<T> = { current: T };
type Row = { el: HTMLDivElement; inner: HTMLDivElement; order: number; y: number; h: number; removing: boolean };
export default function TableRowDeleteCollapse() {
const rootRef = useRef(null) as unknown as Box<HTMLDivElement | null>;
useEffect(() => {
const root = rootRef.current;
if (!root) return;
// 상수: 행 높이(px) · 위치 보간 속도 · 높이 보간 속도 · 슬롯 수 · 자동 삭제 간격
const ROW_H = 34;
const POS_LERP = 0.22;
const H_LERP = 0.26;
const SLOTS = 4;
const AUTO_DELETE_MS = 1300;
let seq = 0;
const rows: Row[] = [];
let stopped = false;
let rafId = 0;
let autoTimer = 0;
const cleanups: Array<() => void> = [];
function makeRow(label: number) {
const el = document.createElement("div");
el.className = styles.row;
const inner = document.createElement("div");
inner.className = styles.inner;
const cell = document.createElement("span");
cell.className = styles.cell;
cell.textContent = "작업 " + label;
const btn = document.createElement("button");
btn.className = styles.x;
btn.type = "button";
btn.setAttribute("aria-label", "삭제");
btn.textContent = "×";
inner.appendChild(cell);
inner.appendChild(btn);
el.appendChild(inner);
root!.appendChild(el);
return { el, inner };
}
function remove(entry: Row) {
if (entry.removing) return;
entry.removing = true;
rows.forEach((r) => {
if (!r.removing && r.order > entry.order) r.order -= 1;
});
}
function addRow(entering: boolean) {
seq++;
const made = makeRow(seq);
const order = rows.filter((r) => !r.removing).length;
const entry: Row = { el: made.el, inner: made.inner, order, y: order * ROW_H, h: entering ? 0 : ROW_H, removing: false };
const btn = made.inner.querySelector(`.${styles.x}`) as HTMLButtonElement;
function onClick(e: Event) {
e.stopPropagation();
root!.classList.remove(styles["is-demo"]);
remove(entry);
}
btn.addEventListener("click", onClick);
cleanups.push(() => btn.removeEventListener("click", onClick));
rows.push(entry);
return entry;
}
function tick() {
if (stopped) return;
for (let i = rows.length - 1; i >= 0; i--) {
const r = rows[i];
const targetY = r.order * ROW_H;
r.y += (targetY - r.y) * POS_LERP;
const targetH = r.removing ? 0 : ROW_H;
r.h += (targetH - r.h) * H_LERP;
r.el.style.transform = "translateY(" + r.y.toFixed(1) + "px)";
r.inner.style.height = Math.max(0, r.h).toFixed(1) + "px";
r.inner.style.opacity = (r.h / ROW_H).toFixed(2);
if (r.removing && r.h < 0.5) {
r.el.remove();
rows.splice(i, 1);
}
}
rafId = requestAnimationFrame(tick);
}
rafId = requestAnimationFrame(tick);
addRow(false); addRow(false); addRow(false); addRow(false);
root.style.height = (SLOTS * ROW_H) + "px";
function autoStep() {
if (stopped) return;
if (root!.classList.contains(styles["is-demo"]) && rows.length) {
remove(rows[0]);
addRow(true);
}
autoTimer = window.setTimeout(autoStep, AUTO_DELETE_MS);
}
autoTimer = window.setTimeout(autoStep, AUTO_DELETE_MS);
return () => {
stopped = true;
cancelAnimationFrame(rafId);
window.clearTimeout(autoTimer);
cleanups.forEach((fn) => fn());
rows.forEach((r) => r.el.remove());
};
}, []);
return <div ref={rootRef} className={`${styles.stage} ${styles.tdc} ${styles["is-demo"]}`} role="table" aria-label="삭제 가능한 작업 표" />;
}05 행 삭제 값과 높이 값 동시 변화 — 행의 존재 값이 사라지면 그 행의 높이 값이 0으로 줄며 아래 행들의 위치 값이 빈자리를 채우는 방식 — 측정 누적면적 5.46% · 움직인 프레임 6/23GODRICH 원작 · zip 에 SCSS·React 포함 · 아코디언 테이블 9종 — 행이 펼쳐지고 접히는 아홉 가지 방식
이 데모가 속한 글
아코디언 테이블 9종 — 행이 펼쳐지고 접히는 아홉 가지 방식아코디언 테이블은 표의 행 하나가 열리고 닫히는 상태에 따라 주변 값이 함께 바뀌는 인터랙션인데, 아홉 개 중 하나는 행의 확장 값이 바뀌면 그 아래 상세 영역의 높이 값이 함께 늘어나고, 하나는 행의 존재 값이 사 이 글의 데모 전부 보기


