diff --git a/package.json b/package.json index 40c443583..68e3d6ee9 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@netdata/netdata-ui", - "version": "5.4.16", + "version": "5.4.17", "description": "netdata UI kit", "main": "dist/index.js", "module": "dist/es6/index.js", diff --git a/src/components/table/body/columnFlex.js b/src/components/table/body/columnFlex.js new file mode 100644 index 000000000..5034d27be --- /dev/null +++ b/src/components/table/body/columnFlex.js @@ -0,0 +1,5 @@ +export default (column, header, hasExplicitSize) => + (column.columnDef.notFlex || column.getCanResize()) && + (!column.columnDef.fullWidth || hasExplicitSize) + ? false + : header.colSpan diff --git a/src/components/table/body/header/cell.js b/src/components/table/body/header/cell.js index 3f83b32ff..cd5062232 100644 --- a/src/components/table/body/header/cell.js +++ b/src/components/table/body/header/cell.js @@ -5,6 +5,7 @@ import Flex from "@/components/templates/flex" import { Text } from "@/components/typography" import { useTableState } from "../../provider" import ResizeHandler from "./resizeHandler" +import getColumnFlex from "../columnFlex" import Sorting, { SortIconContainer } from "./sorting" import Info from "./info" import Filter from "./filter" @@ -73,11 +74,7 @@ const BodyHeaderCell = ({ s + h.column.getSize(), 0) : column.getSize()}px`} height={{ min: "45px" }} position="relative" @@ -125,7 +122,7 @@ const BodyHeaderCell = ({ - + {children} ) diff --git a/src/components/table/body/header/resizeHandler.js b/src/components/table/body/header/resizeHandler.js index fca349170..a52243b85 100644 --- a/src/components/table/body/header/resizeHandler.js +++ b/src/components/table/body/header/resizeHandler.js @@ -1,10 +1,26 @@ -import React from "react" +import React, { useRef } from "react" +import { flushSync } from "react-dom" import styled from "styled-components" import Flex from "@/components/templates/flex" import { useTableState } from "../../provider" const rerenderSelector = state => state.columnSizingInfo?.deltaPercentage +const AUTO_FIT_PADDING = 8 + +const AUTO_FIT_MAX_RATIO = 0.5 + +const naturalWidthStyles = { + width: "max-content", + minWidth: "max-content", + maxWidth: "none", + whiteSpace: "nowrap", + overflow: "visible", + textOverflow: "clip", + flexWrap: "nowrap", + flexShrink: "0", +} + export const Handler = styled(Flex).attrs({ position: "absolute", top: "2px", @@ -12,16 +28,93 @@ export const Handler = styled(Flex).attrs({ bottom: "2px", })`` -const ResizeHandler = ({ header, table }) => { +const measureNaturalWidth = (nodes, forceNowrap) => { + const probe = document.createElement("div") + probe.style.cssText = [ + "position:absolute", + "visibility:hidden", + "pointer-events:none", + "left:-9999px", + "top:0", + "width:max-content", + "min-width:max-content", + "max-width:none", + "white-space:nowrap", + ].join(";") + document.body.appendChild(probe) + + let max = 0 + try { + nodes.forEach(node => { + const clone = node.cloneNode(true) + clone.style.width = "max-content" + clone.style.minWidth = "max-content" + clone.style.maxWidth = "none" + + const cs = getComputedStyle(node) + clone.style.fontFamily = cs.fontFamily + clone.style.fontSize = cs.fontSize + clone.style.fontWeight = cs.fontWeight + clone.style.fontStyle = cs.fontStyle + clone.style.letterSpacing = cs.letterSpacing + + if (forceNowrap) { + clone.querySelectorAll("*").forEach(el => Object.assign(el.style, naturalWidthStyles)) + Object.assign(clone.style, naturalWidthStyles) + } + + probe.appendChild(clone) + max = Math.max(max, clone.getBoundingClientRect().width, clone.scrollWidth) + probe.removeChild(clone) + }) + } finally { + document.body.removeChild(probe) + } + + return max +} + +const ResizeHandler = ({ header, table, testPrefix = "" }) => { useTableState(rerenderSelector) + const ref = useRef() if (!header.column.getCanResize()) return null - const resizingProps = header.column.getIsResizing() + const { column } = header + + const resizingProps = column.getIsResizing() ? { transform: `translateX(${table.getState().columnSizingInfo.deltaOffset}px)` } : {} + const handleResizeStart = e => { + if (column.columnDef.fullWidth && table.getState().columnSizing?.[column.id] == null) { + const width = ref.current?.parentElement?.getBoundingClientRect().width + if (width) flushSync(() => table.setColumnSizing(old => ({ ...old, [column.id]: width }))) + } + header.getResizeHandler()(e) + } + + const handleAutoFit = () => { + const headerCell = ref.current?.parentElement + if (!headerCell) return + + const container = headerCell.closest(`[data-testid="netdata-table${testPrefix}"]`) + const cells = container + ? [...container.querySelectorAll('[data-testid^="netdata-table-cell-"]')].filter( + el => el.getAttribute("data-testid") === `netdata-table-cell-${column.id}${testPrefix}` + ) + : [] + + const max = measureNaturalWidth([headerCell, ...cells], !column.columnDef.fullWidth) + if (!max) return + + const limit = container ? container.clientWidth * AUTO_FIT_MAX_RATIO : Infinity + const width = Math.min(Math.ceil(max) + AUTO_FIT_PADDING, limit) + table.setColumnSizing(old => ({ ...old, [column.id]: width })) + } + return ( { cursor: "col-resize", ...resizingProps, }} - onMouseDown={header.getResizeHandler()} - onTouchStart={header.getResizeHandler()} + onMouseDown={handleResizeStart} + onTouchStart={handleResizeStart} + onDoubleClick={handleAutoFit} /> ) } diff --git a/src/components/table/body/row.js b/src/components/table/body/row.js index 9b406cac7..cf63849c3 100644 --- a/src/components/table/body/row.js +++ b/src/components/table/body/row.js @@ -3,8 +3,9 @@ import styled from "styled-components" import Flex from "@/components/templates/flex" import { getColor } from "@/theme" import { useTableState } from "../provider" +import getColumnFlex from "./columnFlex" -const CellGroup = ({ cell, row, header, testPrefix, coloredSortedColumn }) => { +const CellGroup = ({ cell, row, table, header, testPrefix, coloredSortedColumn }) => { const { column } = cell const tableMeta = useMemo( @@ -32,11 +33,7 @@ const CellGroup = ({ cell, row, header, testPrefix, coloredSortedColumn }) => { return ( ({ columnVisibility: state.columnVisibility, selectedRows: state.selectedRows, allColumns: state.allColumns?.length, + visibleColumns: state.visibleColumns, }) const StyledRow = styled(Flex)` @@ -147,6 +145,7 @@ export default memo( { { rowsById: table.getRowModel().rowsById, selectedRows: table.getSelectedRowModel().flatRows, allColumns: table?.getAllLeafColumns?.(), + visibleColumns: table?.getVisibleLeafColumns?.().map(column => column.id), }) }, [table.getState()])