0.1.3
컬럼을 고정했을 때 선택·복사가 화면에 보이는 순서를 따르도록 좌표계를 통일하고, 활성 셀 가로 추적과 다중 그리드 포커스 문제를 고칩니다.
고정한 컬럼이 선택·복사에서 화면과 다른 자리에 있던 문제
컬럼을 고정하면 그 컬럼은 화면에서 자리를 옮깁니다 — 좌측 고정은 맨 앞, 우측 고정은 맨 뒤로. 그런데 좌표계(선택 범위·복사·붙여넣기·채우기·aria-colindex)는 컬럼 정의 순서를 그대로 쓰고 있어서, 정의 순서 한가운데 있던 컬럼을 고정하면 화면과 좌표가 갈라졌습니다.
정의 순서 a b c d e
c를 좌측 고정
화면 c │ a b d e
좌표계 a b c d e이 배치에서 c부터 b까지 드래그하면 화면상 사이에 있는 a가 빠진 채 c와 b만 선택됐습니다. 선택 사각형에 시각적으로 구멍이 뚫린 셈입니다. 복사도 같은 순서를 써서, 드래그한 영역과 붙여넣은 결과의 컬럼 순서가 달랐습니다.
이제 렌더·병합·좌표계가 모두 화면에 보이는 순서 하나를 씁니다. 어떤 고정 상태에서도 선택 범위는 화면에서 끊긴 곳 없는 직사각형이고, 복사한 TSV의 컬럼 순서는 화면 왼→오와 같습니다. AG Grid가 범위에 "gap이 없다"고 규정하는 것과 같은 규약입니다. (엑셀은 틀 고정이 항상 맨 앞 몇 열이라 이 상황 자체가 생기지 않습니다.)
고정을 해제하면 컬럼은 원래 정의 위치로 돌아갑니다. 그리드는 순서를 파생만 하고 columnOrder 상태를 바꾸지 않습니다 — 컬럼 순서는 소비자가 소유합니다.
컬럼 재정렬(columnOrder)과 함께 쓰면 둘이 합성됩니다. 재정렬이 먼저 적용되고, 고정 컬럼이 그 순서에서 양 끝으로 뽑혀 나갑니다.
동작 변경 — serializeRangeToTsv가 내보내는 컬럼 순서가 정의 순서에서 화면 순서로 바뀝니다. 고정을 쓰지 않거나
정의 순서의 앞/뒤 컬럼만 고정하는 구성에서는 출력이 이전과 같습니다. 가운데 컬럼을 고정한 경우에만 달라지며,
그 경우 지금까지의 출력이 화면과 어긋나 있었습니다.
aria-colindex가 시각 위치를 가리킵니다
ARIA grid는 aria-colindex를 시각 위치로 정의합니다. 정의 순서를 쓰던 동안에는 가운데 컬럼을 고정하면 스크린 리더에 실제와 다른 좌표가 전달됐습니다. 이제 헤더와 셀이 같은 표시 순서를 씁니다.
고정을 런타임에 바꿔도 병합이 따라옵니다
가로 병합이 흡수하는 "다음 컬럼"은 화면상 오른쪽 이웃입니다. 병합 계산 결과를 재사용할지 판단하는 기준이 고정 상태를 반영하지 않아, 런타임에 고정을 바꾸면 병합이 이전 경계에 머물렀습니다. 이제 고정이 바뀌면 병합도 다시 계산됩니다. 세로 병합(rowSpan)을 함께 쓰는 그리드에서 나타나던 문제입니다.
로딩 스켈레톤의 컬럼 배치가 실제 행과 같아집니다
로딩 중 표시하는 스켈레톤 행만 정의 순서로 컬럼을 배치해, 가운데 컬럼을 고정한 구성에서 로딩이 끝나는 순간 컬럼이 자리를 바꿨습니다.
displayLeafColumns
직접 컬럼 인덱스를 계산하는 코드(커스텀 클립보드, 컬럼 헤더 클릭 선택 등)를 위해 그리드가 쓰는 순서를 그대로 노출합니다.
import { displayLeafColumns } from '@featuring-corp/data-grid';
const colIds = displayLeafColumns(table).map((c) => c.id);
const inRange = colIds.slice(colIds.indexOf(startId), colIds.indexOf(endId) + 1);TanStack의 table.getVisibleLeafColumns()는 정의 순서라 고정을 반영하지 않습니다. row.getVisibleCells()와 table.getHeaderGroups()는 표시 순서이므로 displayLeafColumns와 일치합니다.
serializeRangeToTsv는 이제 시작/끝이 뒤집힌 range도 정규화해서 받습니다. 이전에는 직접 만든 좌표를 역방향으로 넘기면 빈 문자열이 반환됐습니다.
활성 셀 추적이 가로로 따라가지 않던 문제
useDataGridScrollToActive가 스크롤할 컨테이너를 하나만 골라 가로·세로 이동량을 함께 적용했습니다. 두 축의 주인이 다른 구성에서는 가로 이동량이 가로로 움직일 수 없는 요소에 적용되어 조용히 버려졌습니다. 활성 셀이 오른쪽 화면 밖으로 나가도 아무 일도 일어나지 않고, 오류도 경고도 없었습니다. 세로는 정상이라 더 눈에 띄지 않았습니다.
두 축의 주인이 갈리는 건 특수한 구성이 아닙니다. <DataGrid.Root>의 기본값이 overflow: auto라, 그리드를 세로로 스크롤되는 박스(패널·모달 본문·페이지)에 넣으면 자연히 이렇게 됩니다.
| 축 | 주인 |
|---|---|
| 가로 | <DataGrid.Root> — 컬럼 총폭이 Root보다 넓을 때 |
| 세로 | 바깥 박스 — Root엔 높이 제약이 없어 세로로 넘칠 것이 없을 때 |
이제 세로 이동량은 세로 주인에게, 가로 이동량은 가로 주인에게 각각 적용합니다. 두 축의 주인이 같으면 한 번만 스크롤해 behavior: 'smooth'에서 두 애니메이션이 서로를 끊지 않습니다.
0.1.2에서 "실제 스크롤러가 누구인가"의 해석을 축별로 통일했지만, 활성 셀 추적은 그 결과를 다시 한 요소로 합쳐 쓰고 있었습니다. 이번 버전이 그 마지막 구간을 맞춥니다.
스크롤러 탐색이 실제 스크롤 가능 여부를 봅니다
overflow: auto가 걸려 있어도 그 축으로 넘칠 내용이 없으면 스크롤해봐야 아무 일도 일어나지 않습니다. 이제 축마다 "지금 실제로 움직일 수 있는" 요소를 먼저 찾고, 아무도 여유가 없을 때만 선언된 overflow를 근거로 고릅니다. [data-scroll-container] 마커도 같은 규칙을 따릅니다. 마커가 세로만 쥐는 구성에서 마커가 가로까지 선점해 가로 이동이 죽던 것이 이 문제의 실제 원인이었습니다.
<DataGrid.Root>가 두 축을 모두 쥐는 기본 구성은 해석 결과가 이전과 같아 동작이 그대로입니다.
getScrollContainer가 축별 지정을 받습니다
useDataGridScrollToActive(table, {
selection,
getScrollContainer: (cellEl) => ({
vertical: cellEl.closest<HTMLElement>('[data-scroll-container]'),
horizontal: cellEl.closest<HTMLElement>('[data-grid]'),
}),
});엘리먼트 하나를 반환하면 지금까지처럼 두 축 공통으로 씁니다. 기존 코드는 그대로 두어도 됩니다. 한쪽 축만 담아 반환하면 나머지 축은 자동 탐색이 채웁니다.
활성 셀을 보이게 하려면 어떤 축으로 스크롤해야 하는데 그 축을 쥔 요소가 실제로 움직일 수 없으면, 개발 모드에서 경고를 한 번 출력합니다.
편집을 끝내면 포커스가 다른 그리드로 넘어가던 문제
한 페이지에 그리드가 둘 이상일 때, 아래쪽 그리드에서 편집을 마치면 포커스가 맨 위 그리드로 넘어갔습니다. 편집기가 닫히면서 포커스가 body로 빠지므로 그리드로 되돌려야 하는데, 되돌릴 대상을 좌표로 다시 찾으면서 document 전체를 조회한 것이 원인입니다. rowId와 columnId는 그리드 사이에서 유일하지 않으므로 언제나 문서의 첫 그리드가 잡혔습니다.
증상이 두 갈래로 갈려 원인을 찾기 어려웠습니다.
- 위 그리드에 활성 셀이 있으면 → 입력이 위 그리드로 들어갑니다
- 위 그리드를 한 번도 건드리지 않았으면 → 입력이 아무 데도 들어가지 않습니다
그동안 아래 그리드의 셀 선택 표시는 그대로 남아 있어 정상으로 보입니다.
이제 포커스를 되돌릴 때 좌표로 찾지 않고 그리드가 자기 루트 엘리먼트를 직접 참조합니다. 가상화로 셀이 화면에서 사라졌거나 컬럼 순서가 바뀌어도 대상이 흔들리지 않습니다. 같은 이유로 활성 셀 스크롤의 조회 범위도 자기 그리드 안으로 좁혔습니다.
자기 그리드를 지목할 수 있습니다
<DataGrid.Root>가 data-grid 속성에 그리드 인스턴스 id를 담아 노출합니다. 한 페이지에 그리드가 여럿일 때 소비자 코드도 자기 그리드를 지목할 수 있습니다.
동작 변경 — data-grid 속성이 이전에는 빈 값이었습니다. [data-grid]처럼 속성 존재로 매칭하는 선택자는
그대로 동작하지만, [data-grid=""]처럼 빈 값과 정확히 일치하는 선택자를 쓰고 있다면 더 이상 매칭되지 않습니다.
선택 표시가 고정 컬럼 위로 보이던 문제
복사 점선, 채우기 미리보기, 채우기 핸들, 터치 선택 핸들이 고정 컬럼 위로 떠올랐습니다. 셀을 고정 컬럼 아래로 스크롤해도 이 표시들이 계속 보이고, 터치 선택 핸들은 보이지 않는 자리에서도 잡혔습니다.
원인은 행 구분선입니다. 행 구분선 1px은 행이 그리고 어떤 셀도 그 자리를 소유하지 않아서, 셀 밖으로 삐져나온 표시는 고정 컬럼이 덮을 수 없습니다.
이제 이 표시들을 셀이 직접 그립니다. 복사 점선과 채우기 미리보기는 셀의 가상 요소로, 채우기 핸들과 터치 선택 핸들은 셀의 자식으로 렌더됩니다. 그리는 순서가 곧 잡히는 순서이므로, 고정 컬럼이 덮는 순간 보이는 것과 잡히는 것이 함께 막힙니다.
복사 점선과 채우기 미리보기가 셀 속성이 됩니다
별도의 오버레이 엘리먼트가 사라지고, 셀이 자기 변만 그립니다. 셀에 다음 속성이 붙어 CSS와 테스트에서 잡을 수 있습니다.
| 속성 | 의미 |
|---|---|
data-copied | 복사·잘라내기 범위에 포함 |
data-copied-t data-copied-r data-copied-b data-copied-l | 그 변이 범위의 바깥 경계 |
data-clipboard-mode | copy 또는 cut |
data-fill-preview + -t -r -b -l | 채우기 미리보기 범위 / 그 변이 바깥 경계 |
data-fill-clear | 미리보기가 비우기 밴드 |
data-clipboard-mode는 이전에 오버레이 엘리먼트에 있었고 이제 셀에 붙습니다. 오버레이에만 있던
data-copied-range와 data-clear는 함께 사라졌습니다. 문서화된 적 없는 내부 속성이지만, 이 엘리먼트를
직접 선택자로 잡고 있었다면 위 셀 속성으로 옮겨 주세요.
눈에 보이는 변화
- 채우기 핸들 — 셀 모서리에 걸터앉지 않고 선택 사각형 안쪽 우하단에 붙습니다. 흰 테두리를 안쪽 두 변에만 두어 선택 테두리와 이어져 보입니다.
- 터치 선택 핸들 — 앵커 셀 안에 들어갑니다. 실효 터치 영역은
min(44px, 컬럼 폭) × 행 내용 높이입니다. 이전에는 셀 밖으로 절반이 나가 이웃 행을 침범했고, 그것이 위 문제의 원인이었습니다. - 편집 중인 셀 — 일반 컬럼과 고정 컬럼의 층을 나눕니다. 일반 컬럼의 셀을 편집하면 같은 행의 고정 컬럼이 편집기를 덮습니다. 시트에서 frozen pane이 이기는 것과 같은 규약입니다.
겹침 순서를 문서화했습니다
무엇이 무엇을 덮는지 지금까지 문서에 없었습니다. Architecture — 겹침 순서에 정리했고, Pinning에는 고정 컬럼이 덮는 대상을 표로 실었습니다.
z 값 자체는 내부 구현이라 토큰으로 노출하지 않습니다. 소비자가 기대야 하는 건 숫자가 아니라 순서입니다. 겹침이 어긋나는 상황을 만나면 z-index를 덮어쓰기 전에 알려 주세요.
공개 표면
displayLeafColumns(table)— 화면에 보이는 순서(좌측고정 → center → 우측고정)의 visible leaf 컬럼. 그리드 좌표계가 쓰는 바로 그 순서.ScrollContainerResolution—getScrollContainer의 반환 타입.HTMLElement(두 축 공통) 또는{ vertical, horizontal }(축별).DataGridFillHandle.previewClear— 미리보기가 비우기 밴드인지 여부. 채우기 핸들을 소스 안쪽으로 되끌면true가 되고 미리보기가 muted 톤으로 바뀝니다.