@ceed/cds 1.27.2 → 1.28.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1965,6 +1965,36 @@ import React25, {
1965
1965
  } from "react";
1966
1966
  import { useVirtualizer as useVirtualizer2 } from "@tanstack/react-virtual";
1967
1967
 
1968
+ // src/libs/text-measurer.ts
1969
+ var TextMeasurer = class {
1970
+ constructor(font) {
1971
+ const canvas = document.createElement("canvas");
1972
+ this.ctx = canvas.getContext("2d");
1973
+ if (this.ctx && font) this.ctx.font = font;
1974
+ }
1975
+ setFont(font) {
1976
+ if (this.ctx) this.ctx.font = font;
1977
+ return this;
1978
+ }
1979
+ setFontFromElement(el) {
1980
+ if (this.ctx) this.ctx.font = getComputedStyle(el).font;
1981
+ return this;
1982
+ }
1983
+ measureText(text) {
1984
+ if (!this.ctx) return 0;
1985
+ return this.ctx.measureText(text).width;
1986
+ }
1987
+ measureMaxWidth(values) {
1988
+ if (!this.ctx) return 0;
1989
+ let max = 0;
1990
+ for (let i = 0; i < values.length; i++) {
1991
+ const w = this.ctx.measureText(values[i]).width;
1992
+ if (w > max) max = w;
1993
+ }
1994
+ return max;
1995
+ }
1996
+ };
1997
+
1968
1998
  // src/components/DataTable/utils.ts
1969
1999
  function extractFieldsFromGroupingModel(items) {
1970
2000
  const fields = /* @__PURE__ */ new Set();
@@ -2082,10 +2112,84 @@ function calculateColumnGroups(columnGroupingModel, columns, visibleFields) {
2082
2112
  const correctedMaxLevel = filteredGroupsByLevel.length > 0 ? filteredGroupsByLevel.length - 1 : -1;
2083
2113
  return { groups: filteredGroupsByLevel, maxLevel: correctedMaxLevel, fieldsInGroupingModel };
2084
2114
  }
2115
+ function parsePxValue(value) {
2116
+ if (!value) return null;
2117
+ const trimmed = value.trim();
2118
+ if (trimmed.endsWith("px")) {
2119
+ const num = parseFloat(trimmed);
2120
+ return isNaN(num) ? null : num;
2121
+ }
2122
+ return null;
2123
+ }
2085
2124
  function getTextAlign(props) {
2086
2125
  return !props.editMode && ["number", "date", "currency"].includes(props.type || "") ? "end" : "start";
2087
2126
  }
2088
2127
  var numberFormatter = (value) => "Intl" in window ? new Intl.NumberFormat().format(value) : value;
2128
+ function computeHeaderWidth(headerEl) {
2129
+ const thStyle = getComputedStyle(headerEl);
2130
+ const paddingX = parseFloat(thStyle.paddingLeft) + parseFloat(thStyle.paddingRight);
2131
+ const borderX = parseFloat(thStyle.borderLeftWidth) + parseFloat(thStyle.borderRightWidth);
2132
+ const stack = headerEl.firstElementChild;
2133
+ if (!stack) return paddingX;
2134
+ const stackStyle = getComputedStyle(stack);
2135
+ const gap = parseFloat(stackStyle.gap) || parseFloat(stackStyle.columnGap) || 0;
2136
+ let totalChildWidth = 0;
2137
+ let visibleChildCount = 0;
2138
+ for (const child of Array.from(stack.children)) {
2139
+ const el = child;
2140
+ if (!el.offsetWidth && !el.offsetHeight) continue;
2141
+ visibleChildCount++;
2142
+ const textEl = el.querySelector?.('[data-slot="header-text"]') || (el.dataset?.slot === "header-text" ? el : null);
2143
+ if (textEl) {
2144
+ const htmlEl = textEl;
2145
+ const isMultiLine = getComputedStyle(htmlEl).display === "-webkit-box";
2146
+ if (isMultiLine) {
2147
+ const measurer = new TextMeasurer();
2148
+ measurer.setFontFromElement(htmlEl);
2149
+ totalChildWidth += measurer.measureText(htmlEl.textContent || "");
2150
+ } else {
2151
+ totalChildWidth += htmlEl.scrollWidth;
2152
+ }
2153
+ } else {
2154
+ totalChildWidth += el.offsetWidth;
2155
+ }
2156
+ }
2157
+ const totalGaps = visibleChildCount > 1 ? (visibleChildCount - 1) * gap : 0;
2158
+ return totalChildWidth + totalGaps + paddingX + borderX;
2159
+ }
2160
+ function computeBodyWidth(headerEl, table, field, dataInPage) {
2161
+ const headId = headerEl.id;
2162
+ const sampleTd = headId ? table.querySelector(`tbody td[headers="${CSS.escape(headId)}"]`) : null;
2163
+ const styleSource = sampleTd || headerEl;
2164
+ const tdStyle = getComputedStyle(styleSource);
2165
+ const bodyPaddingX = parseFloat(tdStyle.paddingLeft) + parseFloat(tdStyle.paddingRight);
2166
+ const bodyBorderX = parseFloat(tdStyle.borderLeftWidth) + parseFloat(tdStyle.borderRightWidth);
2167
+ const measurer = new TextMeasurer();
2168
+ measurer.setFont(tdStyle.font);
2169
+ const texts = [];
2170
+ for (let i = 0; i < dataInPage.length; i++) {
2171
+ const val = dataInPage[i][field];
2172
+ texts.push(val == null ? "" : String(val));
2173
+ }
2174
+ const maxTextWidth = measurer.measureMaxWidth(texts);
2175
+ return maxTextWidth + bodyPaddingX + bodyBorderX;
2176
+ }
2177
+ function computeAutoFitWidth(params) {
2178
+ const { headerEl, field, dataInPage } = params;
2179
+ const table = headerEl.closest("table");
2180
+ if (!table) return null;
2181
+ const headerWidth = computeHeaderWidth(headerEl);
2182
+ const bodyWidth = computeBodyWidth(headerEl, table, field, dataInPage);
2183
+ let finalWidth = Math.ceil(Math.max(headerWidth, bodyWidth));
2184
+ const thStyle = getComputedStyle(headerEl);
2185
+ const resolvedMin = thStyle.minWidth;
2186
+ const resolvedMax = thStyle.maxWidth;
2187
+ const minPx = resolvedMin !== "none" ? parseFloat(resolvedMin) : NaN;
2188
+ const maxPx = resolvedMax !== "none" ? parseFloat(resolvedMax) : NaN;
2189
+ if (!isNaN(minPx) && minPx > 0) finalWidth = Math.max(finalWidth, minPx);
2190
+ if (!isNaN(maxPx) && maxPx > 0) finalWidth = Math.min(finalWidth, maxPx);
2191
+ return finalWidth;
2192
+ }
2089
2193
 
2090
2194
  // src/components/DataTable/styled.tsx
2091
2195
  import React17 from "react";
@@ -2196,7 +2300,7 @@ var StyledTd = styled8("td")(({ theme }) => ({
2196
2300
  var MotionSortIcon = motion17(SortIcon);
2197
2301
  var DefaultLoadingOverlay = () => /* @__PURE__ */ React17.createElement(LinearProgress, { value: 8, variant: "plain" });
2198
2302
  var DefaultNoRowsOverlay = () => /* @__PURE__ */ React17.createElement(Typography3, { level: "body-sm", textColor: "text.tertiary" }, "No rows");
2199
- var Resizer = (ref, targetRef = ref, onResizeStateChange) => /* @__PURE__ */ React17.createElement(
2303
+ var Resizer = (ref, targetRef = ref, onResizeStateChange, onAutoFit) => /* @__PURE__ */ React17.createElement(
2200
2304
  Box_default,
2201
2305
  {
2202
2306
  sx: {
@@ -2204,24 +2308,67 @@ var Resizer = (ref, targetRef = ref, onResizeStateChange) => /* @__PURE__ */ Rea
2204
2308
  top: 0,
2205
2309
  right: 0,
2206
2310
  bottom: 0,
2207
- width: "4px",
2208
- cursor: "col-resize"
2311
+ width: "7px",
2312
+ cursor: "col-resize",
2313
+ "&::after": {
2314
+ content: '""',
2315
+ position: "absolute",
2316
+ top: 0,
2317
+ bottom: 0,
2318
+ right: 0,
2319
+ width: "2px",
2320
+ bgcolor: "transparent",
2321
+ transition: "background-color 150ms ease"
2322
+ },
2323
+ "&:hover::after": {
2324
+ bgcolor: "primary.300"
2325
+ },
2326
+ "&[data-resizing]::after": {
2327
+ bgcolor: "primary.500"
2328
+ }
2209
2329
  },
2210
2330
  onClick: (e) => e.stopPropagation(),
2331
+ onDoubleClick: (e) => {
2332
+ e.stopPropagation();
2333
+ e.preventDefault();
2334
+ onAutoFit?.();
2335
+ },
2211
2336
  onMouseDown: (e) => {
2337
+ if (e.detail >= 2) return;
2338
+ const resizerEl = e.currentTarget;
2339
+ resizerEl.dataset.resizing = "";
2212
2340
  const initialX = e.clientX;
2213
2341
  const initialWidth = ref.current?.getBoundingClientRect().width;
2214
- onResizeStateChange?.(true);
2342
+ let activated = false;
2343
+ const DRAG_THRESHOLD = 3;
2344
+ const thStyle = ref.current ? getComputedStyle(ref.current) : null;
2345
+ const minW = thStyle ? parseFloat(thStyle.minWidth) : NaN;
2346
+ const maxW = thStyle ? parseFloat(thStyle.maxWidth) : NaN;
2215
2347
  const onMouseMove = (e2) => {
2216
- if (initialWidth && initialX) {
2217
- ref.current.style.width = `${initialWidth + (e2.clientX - initialX)}px`;
2218
- targetRef.current.style.width = `${initialWidth + (e2.clientX - initialX)}px`;
2348
+ if (!initialWidth) return;
2349
+ const delta = e2.clientX - initialX;
2350
+ if (!activated) {
2351
+ if (Math.abs(delta) < DRAG_THRESHOLD) return;
2352
+ activated = true;
2353
+ onResizeStateChange?.(true);
2354
+ }
2355
+ if (!ref.current || !targetRef.current) {
2356
+ onMouseUp();
2357
+ return;
2219
2358
  }
2359
+ let newWidth = initialWidth + delta;
2360
+ if (!isNaN(minW) && minW > 0) newWidth = Math.max(newWidth, minW);
2361
+ if (!isNaN(maxW) && maxW > 0) newWidth = Math.min(newWidth, maxW);
2362
+ ref.current.style.width = `${newWidth}px`;
2363
+ targetRef.current.style.width = `${newWidth}px`;
2220
2364
  };
2221
2365
  const onMouseUp = () => {
2366
+ resizerEl.removeAttribute("data-resizing");
2222
2367
  document.removeEventListener("mousemove", onMouseMove);
2223
2368
  document.removeEventListener("mouseup", onMouseUp);
2224
- requestAnimationFrame(() => onResizeStateChange?.(false));
2369
+ if (activated) {
2370
+ requestAnimationFrame(() => onResizeStateChange?.(false));
2371
+ }
2225
2372
  };
2226
2373
  document.addEventListener("mousemove", onMouseMove);
2227
2374
  document.addEventListener("mouseup", onMouseUp);
@@ -2803,7 +2950,11 @@ function InfoSign(props) {
2803
2950
  var InfoSign_default = InfoSign;
2804
2951
 
2805
2952
  // src/components/DataTable/components.tsx
2806
- var TextEllipsis = ({ children, lineClamp }) => {
2953
+ var TextEllipsis = ({
2954
+ children,
2955
+ lineClamp,
2956
+ ...rest
2957
+ }) => {
2807
2958
  const textRef = useRef5(null);
2808
2959
  const [showTooltip, setShowTooltip] = useState8(false);
2809
2960
  useLayoutEffect(() => {
@@ -2818,7 +2969,7 @@ var TextEllipsis = ({ children, lineClamp }) => {
2818
2969
  ro.observe(element);
2819
2970
  return () => ro.disconnect();
2820
2971
  }, [children, lineClamp]);
2821
- return /* @__PURE__ */ React22.createElement(Tooltip_default, { title: showTooltip ? children : "", placement: "top" }, /* @__PURE__ */ React22.createElement(EllipsisDiv, { ref: textRef, lineClamp }, children));
2972
+ return /* @__PURE__ */ React22.createElement(Tooltip_default, { title: showTooltip ? children : "", placement: "top" }, /* @__PURE__ */ React22.createElement(EllipsisDiv, { ref: textRef, lineClamp, ...rest }, children));
2822
2973
  };
2823
2974
  var CellTextEllipsis = ({ children }) => {
2824
2975
  const textRef = useRef5(null);
@@ -2870,7 +3021,8 @@ var HeadCell = (props) => {
2870
3021
  headerRef,
2871
3022
  tableColRef,
2872
3023
  headerClassName,
2873
- headerLineClamp
3024
+ headerLineClamp,
3025
+ onAutoFit
2874
3026
  } = props;
2875
3027
  const theme = useTheme();
2876
3028
  const ref = headerRef;
@@ -2887,10 +3039,15 @@ var HeadCell = (props) => {
2887
3039
  );
2888
3040
  const isResizingRef = useRef5(false);
2889
3041
  const resizer = useMemo8(
2890
- () => resizable ?? true ? Resizer(ref, colRef, (isResizing) => {
2891
- isResizingRef.current = isResizing;
2892
- }) : null,
2893
- [resizable, ref, colRef]
3042
+ () => resizable ?? true ? Resizer(
3043
+ ref,
3044
+ colRef,
3045
+ (isResizing) => {
3046
+ isResizingRef.current = isResizing;
3047
+ },
3048
+ onAutoFit ? () => onAutoFit(field) : void 0
3049
+ ) : null,
3050
+ [resizable, ref, colRef, onAutoFit, field]
2894
3051
  );
2895
3052
  const style = useMemo8(
2896
3053
  () => ({
@@ -2978,7 +3135,7 @@ var HeadCell = (props) => {
2978
3135
  initial: "initial",
2979
3136
  className: computedHeaderClassName
2980
3137
  },
2981
- /* @__PURE__ */ React22.createElement(Stack_default, { gap: 1, direction: "row", justifyContent: textAlign, alignItems: "center", sx: { minWidth: 0 } }, textAlign === "end" && sortIcon, textAlign === "end" && infoSign, /* @__PURE__ */ React22.createElement(TextEllipsis, { lineClamp: headerLineClamp }, headerName ?? field, editMode && required && /* @__PURE__ */ React22.createElement(Asterisk, null, "*")), textAlign === "start" && infoSign, textAlign === "start" && sortIcon),
3138
+ /* @__PURE__ */ React22.createElement(Stack_default, { gap: 1, direction: "row", justifyContent: textAlign, alignItems: "center", sx: { minWidth: 0 } }, textAlign === "end" && sortIcon, textAlign === "end" && infoSign, /* @__PURE__ */ React22.createElement(TextEllipsis, { lineClamp: headerLineClamp, "data-slot": "header-text" }, headerName ?? field, editMode && required && /* @__PURE__ */ React22.createElement(Asterisk, null, "*")), textAlign === "start" && infoSign, textAlign === "start" && sortIcon),
2982
3139
  resizer
2983
3140
  );
2984
3141
  };
@@ -3272,7 +3429,8 @@ function useDataTableRenderer({
3272
3429
  isRowSelectable,
3273
3430
  columnGroupingModel,
3274
3431
  columnVisibilityModel,
3275
- onColumnVisibilityModelChange
3432
+ onColumnVisibilityModelChange,
3433
+ checkboxSelection
3276
3434
  }) {
3277
3435
  if (pinnedColumns && columnGroupingModel) {
3278
3436
  throw new Error(
@@ -3305,6 +3463,14 @@ function useDataTableRenderer({
3305
3463
  [reorderedColumns, visibilityModel]
3306
3464
  );
3307
3465
  const visibleFieldSet = useMemo9(() => new Set(visibleColumns.map((c) => c.field)), [visibleColumns]);
3466
+ const tableMinWidth = useMemo9(() => {
3467
+ const DEFAULT_MIN = 50;
3468
+ let total = checkboxSelection ? 40 : 0;
3469
+ for (const col of visibleColumns) {
3470
+ total += parsePxValue(col.minWidth) ?? parsePxValue(col.width) ?? DEFAULT_MIN;
3471
+ }
3472
+ return total;
3473
+ }, [visibleColumns, checkboxSelection]);
3308
3474
  const allColumnsByField = useMemo9(
3309
3475
  () => reorderedColumns.reduce(
3310
3476
  (acc, curr) => ({
@@ -3525,6 +3691,25 @@ function useDataTableRenderer({
3525
3691
  prevRowsRef.current = _rows;
3526
3692
  }
3527
3693
  }, [_rows]);
3694
+ const handleAutoFit = useCallback10(
3695
+ (field) => {
3696
+ const colDef = visibleColumnsByField[field];
3697
+ if (!colDef?.headerRef.current) return;
3698
+ const column = allColumnsByField[field];
3699
+ const columnType = column && "type" in column ? column.type : void 0;
3700
+ if (columnType === "actions") return;
3701
+ const optimalWidth = computeAutoFitWidth({
3702
+ headerEl: colDef.headerRef.current,
3703
+ field,
3704
+ dataInPage
3705
+ });
3706
+ if (optimalWidth == null) return;
3707
+ const widthPx = `${optimalWidth}px`;
3708
+ colDef.headerRef.current.style.width = widthPx;
3709
+ if (colDef.tableColRef.current) colDef.tableColRef.current.style.width = widthPx;
3710
+ },
3711
+ [visibleColumnsByField, allColumnsByField, dataInPage]
3712
+ );
3528
3713
  return {
3529
3714
  rowCount,
3530
3715
  selectableRowCount,
@@ -3536,6 +3721,7 @@ function useDataTableRenderer({
3536
3721
  BodyRow,
3537
3722
  dataInPage,
3538
3723
  handleSortChange,
3724
+ handleAutoFit,
3539
3725
  isAllSelected,
3540
3726
  isTotalSelected,
3541
3727
  isSelectedRow: useCallback10((model) => selectedModelSet.has(model), [selectedModelSet]),
@@ -3585,6 +3771,7 @@ function useDataTableRenderer({
3585
3771
  ]
3586
3772
  ),
3587
3773
  columns,
3774
+ tableMinWidth,
3588
3775
  processedColumnGroups,
3589
3776
  onTotalSelect: useCallback10(() => {
3590
3777
  const selectableRows = rows.filter((row, i) => {
@@ -3966,6 +4153,7 @@ function Component(props, apiRef) {
3966
4153
  pageSize,
3967
4154
  onPaginationModelChange,
3968
4155
  handleSortChange,
4156
+ handleAutoFit,
3969
4157
  dataInPage,
3970
4158
  isTotalSelected,
3971
4159
  focusedRowId,
@@ -3973,6 +4161,7 @@ function Component(props, apiRef) {
3973
4161
  onTotalSelect,
3974
4162
  HeadCell: HeadCell2,
3975
4163
  BodyRow: BodyRow2,
4164
+ tableMinWidth,
3976
4165
  // For keyboard selection
3977
4166
  selectionAnchor,
3978
4167
  setSelectionAnchor
@@ -4162,7 +4351,7 @@ function Component(props, apiRef) {
4162
4351
  ref: parentRef,
4163
4352
  ...backgroundProps
4164
4353
  },
4165
- /* @__PURE__ */ React25.createElement(Table, { ...innerProps }, /* @__PURE__ */ React25.createElement("colgroup", null, checkboxSelection && /* @__PURE__ */ React25.createElement(
4354
+ /* @__PURE__ */ React25.createElement(Table, { ...innerProps, sx: { ...innerProps.sx, minWidth: tableMinWidth } }, /* @__PURE__ */ React25.createElement("colgroup", null, checkboxSelection && /* @__PURE__ */ React25.createElement(
4166
4355
  "col",
4167
4356
  {
4168
4357
  style: {
@@ -4175,7 +4364,8 @@ function Component(props, apiRef) {
4175
4364
  ref: c.tableColRef,
4176
4365
  key: `${c.field.toString()}_${c.width}`,
4177
4366
  style: {
4178
- width: c.width
4367
+ width: c.width,
4368
+ minWidth: c.minWidth ?? "50px"
4179
4369
  }
4180
4370
  }
4181
4371
  ))), /* @__PURE__ */ React25.createElement("thead", null, processedColumnGroups && processedColumnGroups.groups.length > 0 && processedColumnGroups.groups.map((levelGroups, level) => /* @__PURE__ */ React25.createElement("tr", { key: `group-level-${level}` }, checkboxSelection && level === 0 && /* @__PURE__ */ React25.createElement(
@@ -4244,6 +4434,7 @@ function Component(props, apiRef) {
4244
4434
  stickyHeader: props.stickyHeader,
4245
4435
  editMode: !!c.isCellEditable,
4246
4436
  onSortChange: handleSortChange,
4437
+ onAutoFit: handleAutoFit,
4247
4438
  ...c
4248
4439
  }
4249
4440
  )
@@ -0,0 +1,8 @@
1
+ export declare class TextMeasurer {
2
+ private ctx;
3
+ constructor(font?: string);
4
+ setFont(font: string): this;
5
+ setFontFromElement(el: Element): this;
6
+ measureText(text: string): number;
7
+ measureMaxWidth(values: string[]): number;
8
+ }