Skip to content

Commit 968ec8c

Browse files
committed
Column resize double click and e2e test fixes
1 parent 8078deb commit 968ec8c

6 files changed

Lines changed: 223 additions & 24 deletions

File tree

e2e_playwright/pivot_table_test.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -350,7 +350,10 @@ def test_toggle_row_totals_off(page_at_app: Page):
350350
)
351351
expect(container.get_by_test_id("pivot-row-total")).to_have_count(0, timeout=10000)
352352

353-
container.get_by_test_id("toolbar-row-totals").locator("input").click()
353+
panel = open_settings_popover(page, container)
354+
panel.get_by_test_id("toolbar-row-totals").locator("input").evaluate(
355+
"el => el.click()"
356+
)
354357
expect(container.get_by_test_id("pivot-row-total").first).to_be_visible(
355358
timeout=10000
356359
)

streamlit_pivot/frontend/src/PivotRoot.tsx

Lines changed: 9 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -218,7 +218,9 @@ const PivotRoot: FC<PivotRootProps> = ({
218218
}, [pivotData, currentConfig]);
219219

220220
useEffect(() => {
221-
setIsTableScrollable(Boolean(budget?.needsVirtualization));
221+
if (budget?.needsVirtualization) {
222+
setIsTableScrollable(true);
223+
}
222224
}, [budget?.needsVirtualization]);
223225

224226
const renderStartRef = useRef(0);
@@ -614,22 +616,18 @@ const PivotRoot: FC<PivotRootProps> = ({
614616
isFullscreen ? windowHeight : (max_height ?? 500)
615617
}
616618
onSortChange={
617-
currentConfig.interactive && !locked
618-
? handleSortChange
619-
: undefined
619+
currentConfig.interactive ? handleSortChange : undefined
620620
}
621621
onFilterChange={
622-
currentConfig.interactive && !locked
623-
? handleFilterChange
624-
: undefined
622+
currentConfig.interactive ? handleFilterChange : undefined
625623
}
626624
onConfigChange={
627625
currentConfig.interactive && !locked
628626
? handleConfigChange
629627
: undefined
630628
}
631629
onShowValuesAsChange={
632-
currentConfig.interactive && !locked
630+
currentConfig.interactive
633631
? handleShowValuesAsChange
634632
: undefined
635633
}
@@ -651,22 +649,18 @@ const PivotRoot: FC<PivotRootProps> = ({
651649
}
652650
maxRows={safeMaxRows}
653651
onSortChange={
654-
currentConfig.interactive && !locked
655-
? handleSortChange
656-
: undefined
652+
currentConfig.interactive ? handleSortChange : undefined
657653
}
658654
onFilterChange={
659-
currentConfig.interactive && !locked
660-
? handleFilterChange
661-
: undefined
655+
currentConfig.interactive ? handleFilterChange : undefined
662656
}
663657
onConfigChange={
664658
currentConfig.interactive && !locked
665659
? handleConfigChange
666660
: undefined
667661
}
668662
onShowValuesAsChange={
669-
currentConfig.interactive && !locked
663+
currentConfig.interactive
670664
? handleShowValuesAsChange
671665
: undefined
672666
}

streamlit_pivot/frontend/src/renderers/TableRenderer.module.css

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -222,13 +222,13 @@
222222
}
223223

224224
.headerCellInner:has(.headerMenuBtn) {
225-
padding-right: 16px;
225+
padding-right: 20px;
226226
}
227227

228228
.headerMenuBtn {
229229
display: inline-flex;
230230
position: absolute;
231-
right: -6px;
231+
right: -2px;
232232
top: 50%;
233233
transform: translateY(-50%);
234234
width: 20px;

streamlit_pivot/frontend/src/renderers/TableRenderer.test.tsx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1431,21 +1431,21 @@ describe("Column resize handles", () => {
14311431
});
14321432
});
14331433

1434-
it("renders resize handles on last value label per slot when multi-value", () => {
1434+
it("renders resize handles on value labels when multi-value", () => {
14351435
const config = makeConfig({ values: ["revenue", "profit"] });
14361436
const pd = createPivotData(SAMPLE_DATA, config);
14371437
render(<TableRenderer pivotData={pd} config={config} />);
14381438

1439-
const valHandles = screen.getAllByTestId(/^resize-handle-val-\d+$/);
1439+
const valHandles = screen.getAllByTestId(/^resize-handle-val-\d+-\d+$/);
14401440
expect(valHandles.length).toBeGreaterThanOrEqual(1);
14411441
});
14421442

1443-
it("does not render resize handles when no values are provided", () => {
1443+
it("does not render resize handles when single value", () => {
14441444
const config = makeConfig({ values: ["revenue"] });
14451445
const pd = createPivotData(SAMPLE_DATA, config);
14461446
render(<TableRenderer pivotData={pd} config={config} />);
14471447

1448-
const valHandles = screen.queryAllByTestId(/^resize-handle-val-\d+$/);
1448+
const valHandles = screen.queryAllByTestId(/^resize-handle-val-\d+-\d+$/);
14491449
expect(valHandles.length).toBe(0);
14501450
});
14511451

streamlit_pivot/frontend/src/renderers/TableRenderer.tsx

Lines changed: 112 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -480,6 +480,10 @@ export function renderColumnHeaders(
480480
) => void,
481481
columnWidthMap?: Map<number, number>,
482482
headerRowOffsets?: number[],
483+
onResizeDoubleClick?: (
484+
colSlotIndex: number,
485+
e: React.MouseEvent<HTMLDivElement>,
486+
) => void,
483487
): ReactElement[] {
484488
const renderedValueFields = getRenderedValueFields(config);
485489
const visibleSlots = colRange
@@ -730,6 +734,11 @@ export function renderColumnHeaders(
730734
e.stopPropagation();
731735
onResizeMouseDown(rowDimResizeIdx, e);
732736
}}
737+
onDoubleClick={
738+
onResizeDoubleClick
739+
? (e) => onResizeDoubleClick(rowDimResizeIdx, e)
740+
: undefined
741+
}
733742
onMouseEnter={elevateCell}
734743
onMouseLeave={resetCell}
735744
/>
@@ -916,6 +925,11 @@ export function renderColumnHeaders(
916925
e.stopPropagation();
917926
onResizeMouseDown(slotIdx, e);
918927
}}
928+
onDoubleClick={
929+
onResizeDoubleClick
930+
? (e) => onResizeDoubleClick(slotIdx, e)
931+
: undefined
932+
}
919933
onMouseEnter={elevateCell}
920934
onMouseLeave={resetCell}
921935
/>
@@ -991,14 +1005,19 @@ export function renderColumnHeaders(
9911005
/>
9921006
)}
9931007
</div>
994-
{onResizeMouseDown && isLastVal && (
1008+
{onResizeMouseDown && (
9951009
<div
9961010
className={styles.resizeHandle}
997-
data-testid={`resize-handle-val-${slotOffset + si}`}
1011+
data-testid={`resize-handle-val-${slotOffset + si}-${vfi}`}
9981012
onMouseDown={(e) => {
9991013
e.stopPropagation();
10001014
onResizeMouseDown(slotOffset + si, e);
10011015
}}
1016+
onDoubleClick={
1017+
onResizeDoubleClick
1018+
? (e) => onResizeDoubleClick(slotOffset + si, e)
1019+
: undefined
1020+
}
10021021
onMouseEnter={elevateCell}
10031022
onMouseLeave={resetCell}
10041023
/>
@@ -1905,8 +1924,98 @@ const TableRenderer: FC<TableRendererProps> = ({
19051924
startWidth: number;
19061925
} | null>(null);
19071926

1927+
const handleResizeDoubleClick = useCallback(
1928+
(slotIndex: number, e: React.MouseEvent<HTMLDivElement>) => {
1929+
e.preventDefault();
1930+
e.stopPropagation();
1931+
1932+
const th = (e.target as HTMLElement).closest("th");
1933+
const table = wrapperRef.current?.querySelector("table");
1934+
if (!th || !table) return;
1935+
1936+
const headerRow = th.parentElement as HTMLTableRowElement;
1937+
let physicalCol = 0;
1938+
for (const cell of headerRow.cells) {
1939+
if (cell === th) break;
1940+
physicalCol += cell.colSpan || 1;
1941+
}
1942+
1943+
let maxWidth = MIN_COL_WIDTH;
1944+
for (const row of table.querySelectorAll("tbody tr")) {
1945+
const cells = (row as HTMLTableRowElement).cells;
1946+
let col = 0;
1947+
for (const cell of cells) {
1948+
const span = cell.colSpan || 1;
1949+
if (col <= physicalCol && physicalCol < col + span) {
1950+
const ruler = document.createElement("span");
1951+
ruler.style.visibility = "hidden";
1952+
ruler.style.position = "absolute";
1953+
ruler.style.whiteSpace = "nowrap";
1954+
ruler.style.font = getComputedStyle(cell).font;
1955+
ruler.textContent = cell.textContent;
1956+
document.body.appendChild(ruler);
1957+
const w = Math.ceil(ruler.getBoundingClientRect().width);
1958+
document.body.removeChild(ruler);
1959+
1960+
const cs = getComputedStyle(cell);
1961+
const padH =
1962+
parseFloat(cs.paddingLeft) + parseFloat(cs.paddingRight);
1963+
const natural = Math.ceil(w + padH) + 2;
1964+
const perCol = Math.ceil(natural / span);
1965+
if (perCol > maxWidth) maxWidth = perCol;
1966+
break;
1967+
}
1968+
col += span;
1969+
}
1970+
}
1971+
1972+
const headerCells = th.closest("thead")?.querySelectorAll("th");
1973+
if (headerCells) {
1974+
for (const hc of headerCells) {
1975+
const hRow = hc.parentElement as HTMLTableRowElement;
1976+
let hCol = 0;
1977+
for (const cell of hRow.cells) {
1978+
if (cell === hc) break;
1979+
hCol += cell.colSpan || 1;
1980+
}
1981+
const hSpan = hc.colSpan || 1;
1982+
if (hCol <= physicalCol && physicalCol < hCol + hSpan) {
1983+
const inner =
1984+
hc.querySelector<HTMLElement>("[class*='headerCellInner']") ||
1985+
hc.querySelector<HTMLElement>("[class*='valueLabel']") ||
1986+
hc;
1987+
const ruler = document.createElement("span");
1988+
ruler.style.visibility = "hidden";
1989+
ruler.style.position = "absolute";
1990+
ruler.style.whiteSpace = "nowrap";
1991+
ruler.style.font = getComputedStyle(inner).font;
1992+
ruler.textContent = inner.textContent;
1993+
document.body.appendChild(ruler);
1994+
const w = Math.ceil(ruler.getBoundingClientRect().width);
1995+
document.body.removeChild(ruler);
1996+
1997+
const cs = getComputedStyle(hc);
1998+
const padH =
1999+
parseFloat(cs.paddingLeft) + parseFloat(cs.paddingRight);
2000+
const natural = Math.ceil(w + padH) + 18;
2001+
const perCol = Math.ceil(natural / hSpan);
2002+
if (perCol > maxWidth) maxWidth = perCol;
2003+
}
2004+
}
2005+
}
2006+
2007+
setColumnWidthMap((prev) => {
2008+
const next = new Map(prev);
2009+
next.set(slotIndex, maxWidth);
2010+
return next;
2011+
});
2012+
},
2013+
[],
2014+
);
2015+
19082016
const handleResizeMouseDown = useCallback(
19092017
(slotIndex: number, e: React.MouseEvent<HTMLDivElement>) => {
2018+
if (e.detail >= 2) return;
19102019
e.preventDefault();
19112020
e.stopPropagation();
19122021
const el = (e.target as HTMLElement).closest("th");
@@ -2247,6 +2356,7 @@ const TableRenderer: FC<TableRendererProps> = ({
22472356
handleResizeMouseDown,
22482357
columnWidthMap,
22492358
headerRowOffsets.length > 1 ? headerRowOffsets : undefined,
2359+
handleResizeDoubleClick,
22502360
)}
22512361
</thead>
22522362
<tbody>

streamlit_pivot/frontend/src/renderers/VirtualizedTableRenderer.tsx

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,8 +104,98 @@ const VirtualizedTableRenderer: FC<VirtualizedTableRendererProps> = ({
104104
startWidth: number;
105105
} | null>(null);
106106

107+
const handleResizeDoubleClick = useCallback(
108+
(slotIndex: number, e: React.MouseEvent<HTMLDivElement>) => {
109+
e.preventDefault();
110+
e.stopPropagation();
111+
112+
const th = (e.target as HTMLElement).closest("th");
113+
const table = wrapperRef.current?.querySelector("table");
114+
if (!th || !table) return;
115+
116+
const headerRow = th.parentElement as HTMLTableRowElement;
117+
let physicalCol = 0;
118+
for (const cell of headerRow.cells) {
119+
if (cell === th) break;
120+
physicalCol += cell.colSpan || 1;
121+
}
122+
123+
let maxWidth = MIN_COL_WIDTH;
124+
for (const row of table.querySelectorAll("tbody tr")) {
125+
const cells = (row as HTMLTableRowElement).cells;
126+
let col = 0;
127+
for (const cell of cells) {
128+
const span = cell.colSpan || 1;
129+
if (col <= physicalCol && physicalCol < col + span) {
130+
const ruler = document.createElement("span");
131+
ruler.style.visibility = "hidden";
132+
ruler.style.position = "absolute";
133+
ruler.style.whiteSpace = "nowrap";
134+
ruler.style.font = getComputedStyle(cell).font;
135+
ruler.textContent = cell.textContent;
136+
document.body.appendChild(ruler);
137+
const w = Math.ceil(ruler.getBoundingClientRect().width);
138+
document.body.removeChild(ruler);
139+
140+
const cs = getComputedStyle(cell);
141+
const padH =
142+
parseFloat(cs.paddingLeft) + parseFloat(cs.paddingRight);
143+
const natural = Math.ceil(w + padH) + 2;
144+
const perCol = Math.ceil(natural / span);
145+
if (perCol > maxWidth) maxWidth = perCol;
146+
break;
147+
}
148+
col += span;
149+
}
150+
}
151+
152+
const headerCells = th.closest("thead")?.querySelectorAll("th");
153+
if (headerCells) {
154+
for (const hc of headerCells) {
155+
const hRow = hc.parentElement as HTMLTableRowElement;
156+
let hCol = 0;
157+
for (const cell of hRow.cells) {
158+
if (cell === hc) break;
159+
hCol += cell.colSpan || 1;
160+
}
161+
const hSpan = hc.colSpan || 1;
162+
if (hCol <= physicalCol && physicalCol < hCol + hSpan) {
163+
const inner =
164+
hc.querySelector<HTMLElement>("[class*='headerCellInner']") ||
165+
hc.querySelector<HTMLElement>("[class*='valueLabel']") ||
166+
hc;
167+
const ruler = document.createElement("span");
168+
ruler.style.visibility = "hidden";
169+
ruler.style.position = "absolute";
170+
ruler.style.whiteSpace = "nowrap";
171+
ruler.style.font = getComputedStyle(inner).font;
172+
ruler.textContent = inner.textContent;
173+
document.body.appendChild(ruler);
174+
const w = Math.ceil(ruler.getBoundingClientRect().width);
175+
document.body.removeChild(ruler);
176+
177+
const cs = getComputedStyle(hc);
178+
const padH =
179+
parseFloat(cs.paddingLeft) + parseFloat(cs.paddingRight);
180+
const natural = Math.ceil(w + padH) + 18;
181+
const perCol = Math.ceil(natural / hSpan);
182+
if (perCol > maxWidth) maxWidth = perCol;
183+
}
184+
}
185+
}
186+
187+
setColumnWidthMap((prev) => {
188+
const next = new Map(prev);
189+
next.set(slotIndex, maxWidth);
190+
return next;
191+
});
192+
},
193+
[],
194+
);
195+
107196
const handleResizeMouseDown = useCallback(
108197
(slotIndex: number, e: React.MouseEvent<HTMLDivElement>) => {
198+
if (e.detail >= 2) return;
109199
e.preventDefault();
110200
e.stopPropagation();
111201
const el = (e.target as HTMLElement).closest("th");
@@ -435,6 +525,7 @@ const VirtualizedTableRenderer: FC<VirtualizedTableRendererProps> = ({
435525
handleResizeMouseDown,
436526
columnWidthMap,
437527
headerRowOffsets.length > 1 ? headerRowOffsets : undefined,
528+
handleResizeDoubleClick,
438529
);
439530
},
440531
[
@@ -450,6 +541,7 @@ const VirtualizedTableRenderer: FC<VirtualizedTableRendererProps> = ({
450541
handleToggleColGroup,
451542
pivotData,
452543
handleResizeMouseDown,
544+
handleResizeDoubleClick,
453545
headerRowOffsets,
454546
],
455547
);

0 commit comments

Comments
 (0)