@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.cjs CHANGED
@@ -2091,6 +2091,36 @@ var CurrencyInput_default = CurrencyInput;
2091
2091
  var import_react28 = __toESM(require("react"));
2092
2092
  var import_react_virtual2 = require("@tanstack/react-virtual");
2093
2093
 
2094
+ // src/libs/text-measurer.ts
2095
+ var TextMeasurer = class {
2096
+ constructor(font) {
2097
+ const canvas = document.createElement("canvas");
2098
+ this.ctx = canvas.getContext("2d");
2099
+ if (this.ctx && font) this.ctx.font = font;
2100
+ }
2101
+ setFont(font) {
2102
+ if (this.ctx) this.ctx.font = font;
2103
+ return this;
2104
+ }
2105
+ setFontFromElement(el) {
2106
+ if (this.ctx) this.ctx.font = getComputedStyle(el).font;
2107
+ return this;
2108
+ }
2109
+ measureText(text) {
2110
+ if (!this.ctx) return 0;
2111
+ return this.ctx.measureText(text).width;
2112
+ }
2113
+ measureMaxWidth(values) {
2114
+ if (!this.ctx) return 0;
2115
+ let max = 0;
2116
+ for (let i = 0; i < values.length; i++) {
2117
+ const w = this.ctx.measureText(values[i]).width;
2118
+ if (w > max) max = w;
2119
+ }
2120
+ return max;
2121
+ }
2122
+ };
2123
+
2094
2124
  // src/components/DataTable/utils.ts
2095
2125
  function extractFieldsFromGroupingModel(items) {
2096
2126
  const fields = /* @__PURE__ */ new Set();
@@ -2208,10 +2238,84 @@ function calculateColumnGroups(columnGroupingModel, columns, visibleFields) {
2208
2238
  const correctedMaxLevel = filteredGroupsByLevel.length > 0 ? filteredGroupsByLevel.length - 1 : -1;
2209
2239
  return { groups: filteredGroupsByLevel, maxLevel: correctedMaxLevel, fieldsInGroupingModel };
2210
2240
  }
2241
+ function parsePxValue(value) {
2242
+ if (!value) return null;
2243
+ const trimmed = value.trim();
2244
+ if (trimmed.endsWith("px")) {
2245
+ const num = parseFloat(trimmed);
2246
+ return isNaN(num) ? null : num;
2247
+ }
2248
+ return null;
2249
+ }
2211
2250
  function getTextAlign(props) {
2212
2251
  return !props.editMode && ["number", "date", "currency"].includes(props.type || "") ? "end" : "start";
2213
2252
  }
2214
2253
  var numberFormatter = (value) => "Intl" in window ? new Intl.NumberFormat().format(value) : value;
2254
+ function computeHeaderWidth(headerEl) {
2255
+ const thStyle = getComputedStyle(headerEl);
2256
+ const paddingX = parseFloat(thStyle.paddingLeft) + parseFloat(thStyle.paddingRight);
2257
+ const borderX = parseFloat(thStyle.borderLeftWidth) + parseFloat(thStyle.borderRightWidth);
2258
+ const stack = headerEl.firstElementChild;
2259
+ if (!stack) return paddingX;
2260
+ const stackStyle = getComputedStyle(stack);
2261
+ const gap = parseFloat(stackStyle.gap) || parseFloat(stackStyle.columnGap) || 0;
2262
+ let totalChildWidth = 0;
2263
+ let visibleChildCount = 0;
2264
+ for (const child of Array.from(stack.children)) {
2265
+ const el = child;
2266
+ if (!el.offsetWidth && !el.offsetHeight) continue;
2267
+ visibleChildCount++;
2268
+ const textEl = el.querySelector?.('[data-slot="header-text"]') || (el.dataset?.slot === "header-text" ? el : null);
2269
+ if (textEl) {
2270
+ const htmlEl = textEl;
2271
+ const isMultiLine = getComputedStyle(htmlEl).display === "-webkit-box";
2272
+ if (isMultiLine) {
2273
+ const measurer = new TextMeasurer();
2274
+ measurer.setFontFromElement(htmlEl);
2275
+ totalChildWidth += measurer.measureText(htmlEl.textContent || "");
2276
+ } else {
2277
+ totalChildWidth += htmlEl.scrollWidth;
2278
+ }
2279
+ } else {
2280
+ totalChildWidth += el.offsetWidth;
2281
+ }
2282
+ }
2283
+ const totalGaps = visibleChildCount > 1 ? (visibleChildCount - 1) * gap : 0;
2284
+ return totalChildWidth + totalGaps + paddingX + borderX;
2285
+ }
2286
+ function computeBodyWidth(headerEl, table, field, dataInPage) {
2287
+ const headId = headerEl.id;
2288
+ const sampleTd = headId ? table.querySelector(`tbody td[headers="${CSS.escape(headId)}"]`) : null;
2289
+ const styleSource = sampleTd || headerEl;
2290
+ const tdStyle = getComputedStyle(styleSource);
2291
+ const bodyPaddingX = parseFloat(tdStyle.paddingLeft) + parseFloat(tdStyle.paddingRight);
2292
+ const bodyBorderX = parseFloat(tdStyle.borderLeftWidth) + parseFloat(tdStyle.borderRightWidth);
2293
+ const measurer = new TextMeasurer();
2294
+ measurer.setFont(tdStyle.font);
2295
+ const texts = [];
2296
+ for (let i = 0; i < dataInPage.length; i++) {
2297
+ const val = dataInPage[i][field];
2298
+ texts.push(val == null ? "" : String(val));
2299
+ }
2300
+ const maxTextWidth = measurer.measureMaxWidth(texts);
2301
+ return maxTextWidth + bodyPaddingX + bodyBorderX;
2302
+ }
2303
+ function computeAutoFitWidth(params) {
2304
+ const { headerEl, field, dataInPage } = params;
2305
+ const table = headerEl.closest("table");
2306
+ if (!table) return null;
2307
+ const headerWidth = computeHeaderWidth(headerEl);
2308
+ const bodyWidth = computeBodyWidth(headerEl, table, field, dataInPage);
2309
+ let finalWidth = Math.ceil(Math.max(headerWidth, bodyWidth));
2310
+ const thStyle = getComputedStyle(headerEl);
2311
+ const resolvedMin = thStyle.minWidth;
2312
+ const resolvedMax = thStyle.maxWidth;
2313
+ const minPx = resolvedMin !== "none" ? parseFloat(resolvedMin) : NaN;
2314
+ const maxPx = resolvedMax !== "none" ? parseFloat(resolvedMax) : NaN;
2315
+ if (!isNaN(minPx) && minPx > 0) finalWidth = Math.max(finalWidth, minPx);
2316
+ if (!isNaN(maxPx) && maxPx > 0) finalWidth = Math.min(finalWidth, maxPx);
2317
+ return finalWidth;
2318
+ }
2215
2319
 
2216
2320
  // src/components/DataTable/styled.tsx
2217
2321
  var import_react19 = __toESM(require("react"));
@@ -2322,7 +2426,7 @@ var StyledTd = (0, import_joy25.styled)("td")(({ theme }) => ({
2322
2426
  var MotionSortIcon = (0, import_framer_motion17.motion)(import_ArrowUpwardRounded.default);
2323
2427
  var DefaultLoadingOverlay = () => /* @__PURE__ */ import_react19.default.createElement(import_joy25.LinearProgress, { value: 8, variant: "plain" });
2324
2428
  var DefaultNoRowsOverlay = () => /* @__PURE__ */ import_react19.default.createElement(import_joy25.Typography, { level: "body-sm", textColor: "text.tertiary" }, "No rows");
2325
- var Resizer = (ref, targetRef = ref, onResizeStateChange) => /* @__PURE__ */ import_react19.default.createElement(
2429
+ var Resizer = (ref, targetRef = ref, onResizeStateChange, onAutoFit) => /* @__PURE__ */ import_react19.default.createElement(
2326
2430
  Box_default,
2327
2431
  {
2328
2432
  sx: {
@@ -2330,24 +2434,67 @@ var Resizer = (ref, targetRef = ref, onResizeStateChange) => /* @__PURE__ */ imp
2330
2434
  top: 0,
2331
2435
  right: 0,
2332
2436
  bottom: 0,
2333
- width: "4px",
2334
- cursor: "col-resize"
2437
+ width: "7px",
2438
+ cursor: "col-resize",
2439
+ "&::after": {
2440
+ content: '""',
2441
+ position: "absolute",
2442
+ top: 0,
2443
+ bottom: 0,
2444
+ right: 0,
2445
+ width: "2px",
2446
+ bgcolor: "transparent",
2447
+ transition: "background-color 150ms ease"
2448
+ },
2449
+ "&:hover::after": {
2450
+ bgcolor: "primary.300"
2451
+ },
2452
+ "&[data-resizing]::after": {
2453
+ bgcolor: "primary.500"
2454
+ }
2335
2455
  },
2336
2456
  onClick: (e) => e.stopPropagation(),
2457
+ onDoubleClick: (e) => {
2458
+ e.stopPropagation();
2459
+ e.preventDefault();
2460
+ onAutoFit?.();
2461
+ },
2337
2462
  onMouseDown: (e) => {
2463
+ if (e.detail >= 2) return;
2464
+ const resizerEl = e.currentTarget;
2465
+ resizerEl.dataset.resizing = "";
2338
2466
  const initialX = e.clientX;
2339
2467
  const initialWidth = ref.current?.getBoundingClientRect().width;
2340
- onResizeStateChange?.(true);
2468
+ let activated = false;
2469
+ const DRAG_THRESHOLD = 3;
2470
+ const thStyle = ref.current ? getComputedStyle(ref.current) : null;
2471
+ const minW = thStyle ? parseFloat(thStyle.minWidth) : NaN;
2472
+ const maxW = thStyle ? parseFloat(thStyle.maxWidth) : NaN;
2341
2473
  const onMouseMove = (e2) => {
2342
- if (initialWidth && initialX) {
2343
- ref.current.style.width = `${initialWidth + (e2.clientX - initialX)}px`;
2344
- targetRef.current.style.width = `${initialWidth + (e2.clientX - initialX)}px`;
2474
+ if (!initialWidth) return;
2475
+ const delta = e2.clientX - initialX;
2476
+ if (!activated) {
2477
+ if (Math.abs(delta) < DRAG_THRESHOLD) return;
2478
+ activated = true;
2479
+ onResizeStateChange?.(true);
2480
+ }
2481
+ if (!ref.current || !targetRef.current) {
2482
+ onMouseUp();
2483
+ return;
2345
2484
  }
2485
+ let newWidth = initialWidth + delta;
2486
+ if (!isNaN(minW) && minW > 0) newWidth = Math.max(newWidth, minW);
2487
+ if (!isNaN(maxW) && maxW > 0) newWidth = Math.min(newWidth, maxW);
2488
+ ref.current.style.width = `${newWidth}px`;
2489
+ targetRef.current.style.width = `${newWidth}px`;
2346
2490
  };
2347
2491
  const onMouseUp = () => {
2492
+ resizerEl.removeAttribute("data-resizing");
2348
2493
  document.removeEventListener("mousemove", onMouseMove);
2349
2494
  document.removeEventListener("mouseup", onMouseUp);
2350
- requestAnimationFrame(() => onResizeStateChange?.(false));
2495
+ if (activated) {
2496
+ requestAnimationFrame(() => onResizeStateChange?.(false));
2497
+ }
2351
2498
  };
2352
2499
  document.addEventListener("mousemove", onMouseMove);
2353
2500
  document.addEventListener("mouseup", onMouseUp);
@@ -2920,7 +3067,11 @@ function InfoSign(props) {
2920
3067
  var InfoSign_default = InfoSign;
2921
3068
 
2922
3069
  // src/components/DataTable/components.tsx
2923
- var TextEllipsis = ({ children, lineClamp }) => {
3070
+ var TextEllipsis = ({
3071
+ children,
3072
+ lineClamp,
3073
+ ...rest
3074
+ }) => {
2924
3075
  const textRef = (0, import_react24.useRef)(null);
2925
3076
  const [showTooltip, setShowTooltip] = (0, import_react24.useState)(false);
2926
3077
  (0, import_react24.useLayoutEffect)(() => {
@@ -2935,7 +3086,7 @@ var TextEllipsis = ({ children, lineClamp }) => {
2935
3086
  ro.observe(element);
2936
3087
  return () => ro.disconnect();
2937
3088
  }, [children, lineClamp]);
2938
- return /* @__PURE__ */ import_react24.default.createElement(Tooltip_default, { title: showTooltip ? children : "", placement: "top" }, /* @__PURE__ */ import_react24.default.createElement(EllipsisDiv, { ref: textRef, lineClamp }, children));
3089
+ return /* @__PURE__ */ import_react24.default.createElement(Tooltip_default, { title: showTooltip ? children : "", placement: "top" }, /* @__PURE__ */ import_react24.default.createElement(EllipsisDiv, { ref: textRef, lineClamp, ...rest }, children));
2939
3090
  };
2940
3091
  var CellTextEllipsis = ({ children }) => {
2941
3092
  const textRef = (0, import_react24.useRef)(null);
@@ -2987,7 +3138,8 @@ var HeadCell = (props) => {
2987
3138
  headerRef,
2988
3139
  tableColRef,
2989
3140
  headerClassName,
2990
- headerLineClamp
3141
+ headerLineClamp,
3142
+ onAutoFit
2991
3143
  } = props;
2992
3144
  const theme = (0, import_joy32.useTheme)();
2993
3145
  const ref = headerRef;
@@ -3004,10 +3156,15 @@ var HeadCell = (props) => {
3004
3156
  );
3005
3157
  const isResizingRef = (0, import_react24.useRef)(false);
3006
3158
  const resizer = (0, import_react24.useMemo)(
3007
- () => resizable ?? true ? Resizer(ref, colRef, (isResizing) => {
3008
- isResizingRef.current = isResizing;
3009
- }) : null,
3010
- [resizable, ref, colRef]
3159
+ () => resizable ?? true ? Resizer(
3160
+ ref,
3161
+ colRef,
3162
+ (isResizing) => {
3163
+ isResizingRef.current = isResizing;
3164
+ },
3165
+ onAutoFit ? () => onAutoFit(field) : void 0
3166
+ ) : null,
3167
+ [resizable, ref, colRef, onAutoFit, field]
3011
3168
  );
3012
3169
  const style = (0, import_react24.useMemo)(
3013
3170
  () => ({
@@ -3095,7 +3252,7 @@ var HeadCell = (props) => {
3095
3252
  initial: "initial",
3096
3253
  className: computedHeaderClassName
3097
3254
  },
3098
- /* @__PURE__ */ import_react24.default.createElement(Stack_default, { gap: 1, direction: "row", justifyContent: textAlign, alignItems: "center", sx: { minWidth: 0 } }, textAlign === "end" && sortIcon, textAlign === "end" && infoSign, /* @__PURE__ */ import_react24.default.createElement(TextEllipsis, { lineClamp: headerLineClamp }, headerName ?? field, editMode && required && /* @__PURE__ */ import_react24.default.createElement(Asterisk, null, "*")), textAlign === "start" && infoSign, textAlign === "start" && sortIcon),
3255
+ /* @__PURE__ */ import_react24.default.createElement(Stack_default, { gap: 1, direction: "row", justifyContent: textAlign, alignItems: "center", sx: { minWidth: 0 } }, textAlign === "end" && sortIcon, textAlign === "end" && infoSign, /* @__PURE__ */ import_react24.default.createElement(TextEllipsis, { lineClamp: headerLineClamp, "data-slot": "header-text" }, headerName ?? field, editMode && required && /* @__PURE__ */ import_react24.default.createElement(Asterisk, null, "*")), textAlign === "start" && infoSign, textAlign === "start" && sortIcon),
3099
3256
  resizer
3100
3257
  );
3101
3258
  };
@@ -3389,7 +3546,8 @@ function useDataTableRenderer({
3389
3546
  isRowSelectable,
3390
3547
  columnGroupingModel,
3391
3548
  columnVisibilityModel,
3392
- onColumnVisibilityModelChange
3549
+ onColumnVisibilityModelChange,
3550
+ checkboxSelection
3393
3551
  }) {
3394
3552
  if (pinnedColumns && columnGroupingModel) {
3395
3553
  throw new Error(
@@ -3422,6 +3580,14 @@ function useDataTableRenderer({
3422
3580
  [reorderedColumns, visibilityModel]
3423
3581
  );
3424
3582
  const visibleFieldSet = (0, import_react25.useMemo)(() => new Set(visibleColumns.map((c) => c.field)), [visibleColumns]);
3583
+ const tableMinWidth = (0, import_react25.useMemo)(() => {
3584
+ const DEFAULT_MIN = 50;
3585
+ let total = checkboxSelection ? 40 : 0;
3586
+ for (const col of visibleColumns) {
3587
+ total += parsePxValue(col.minWidth) ?? parsePxValue(col.width) ?? DEFAULT_MIN;
3588
+ }
3589
+ return total;
3590
+ }, [visibleColumns, checkboxSelection]);
3425
3591
  const allColumnsByField = (0, import_react25.useMemo)(
3426
3592
  () => reorderedColumns.reduce(
3427
3593
  (acc, curr) => ({
@@ -3642,6 +3808,25 @@ function useDataTableRenderer({
3642
3808
  prevRowsRef.current = _rows;
3643
3809
  }
3644
3810
  }, [_rows]);
3811
+ const handleAutoFit = (0, import_react25.useCallback)(
3812
+ (field) => {
3813
+ const colDef = visibleColumnsByField[field];
3814
+ if (!colDef?.headerRef.current) return;
3815
+ const column = allColumnsByField[field];
3816
+ const columnType = column && "type" in column ? column.type : void 0;
3817
+ if (columnType === "actions") return;
3818
+ const optimalWidth = computeAutoFitWidth({
3819
+ headerEl: colDef.headerRef.current,
3820
+ field,
3821
+ dataInPage
3822
+ });
3823
+ if (optimalWidth == null) return;
3824
+ const widthPx = `${optimalWidth}px`;
3825
+ colDef.headerRef.current.style.width = widthPx;
3826
+ if (colDef.tableColRef.current) colDef.tableColRef.current.style.width = widthPx;
3827
+ },
3828
+ [visibleColumnsByField, allColumnsByField, dataInPage]
3829
+ );
3645
3830
  return {
3646
3831
  rowCount,
3647
3832
  selectableRowCount,
@@ -3653,6 +3838,7 @@ function useDataTableRenderer({
3653
3838
  BodyRow,
3654
3839
  dataInPage,
3655
3840
  handleSortChange,
3841
+ handleAutoFit,
3656
3842
  isAllSelected,
3657
3843
  isTotalSelected,
3658
3844
  isSelectedRow: (0, import_react25.useCallback)((model) => selectedModelSet.has(model), [selectedModelSet]),
@@ -3702,6 +3888,7 @@ function useDataTableRenderer({
3702
3888
  ]
3703
3889
  ),
3704
3890
  columns,
3891
+ tableMinWidth,
3705
3892
  processedColumnGroups,
3706
3893
  onTotalSelect: (0, import_react25.useCallback)(() => {
3707
3894
  const selectableRows = rows.filter((row, i) => {
@@ -4083,6 +4270,7 @@ function Component(props, apiRef) {
4083
4270
  pageSize,
4084
4271
  onPaginationModelChange,
4085
4272
  handleSortChange,
4273
+ handleAutoFit,
4086
4274
  dataInPage,
4087
4275
  isTotalSelected,
4088
4276
  focusedRowId,
@@ -4090,6 +4278,7 @@ function Component(props, apiRef) {
4090
4278
  onTotalSelect,
4091
4279
  HeadCell: HeadCell2,
4092
4280
  BodyRow: BodyRow2,
4281
+ tableMinWidth,
4093
4282
  // For keyboard selection
4094
4283
  selectionAnchor,
4095
4284
  setSelectionAnchor
@@ -4279,7 +4468,7 @@ function Component(props, apiRef) {
4279
4468
  ref: parentRef,
4280
4469
  ...backgroundProps
4281
4470
  },
4282
- /* @__PURE__ */ import_react28.default.createElement(Table, { ...innerProps }, /* @__PURE__ */ import_react28.default.createElement("colgroup", null, checkboxSelection && /* @__PURE__ */ import_react28.default.createElement(
4471
+ /* @__PURE__ */ import_react28.default.createElement(Table, { ...innerProps, sx: { ...innerProps.sx, minWidth: tableMinWidth } }, /* @__PURE__ */ import_react28.default.createElement("colgroup", null, checkboxSelection && /* @__PURE__ */ import_react28.default.createElement(
4283
4472
  "col",
4284
4473
  {
4285
4474
  style: {
@@ -4292,7 +4481,8 @@ function Component(props, apiRef) {
4292
4481
  ref: c.tableColRef,
4293
4482
  key: `${c.field.toString()}_${c.width}`,
4294
4483
  style: {
4295
- width: c.width
4484
+ width: c.width,
4485
+ minWidth: c.minWidth ?? "50px"
4296
4486
  }
4297
4487
  }
4298
4488
  ))), /* @__PURE__ */ import_react28.default.createElement("thead", null, processedColumnGroups && processedColumnGroups.groups.length > 0 && processedColumnGroups.groups.map((levelGroups, level) => /* @__PURE__ */ import_react28.default.createElement("tr", { key: `group-level-${level}` }, checkboxSelection && level === 0 && /* @__PURE__ */ import_react28.default.createElement(
@@ -4361,6 +4551,7 @@ function Component(props, apiRef) {
4361
4551
  stickyHeader: props.stickyHeader,
4362
4552
  editMode: !!c.isCellEditable,
4363
4553
  onSortChange: handleSortChange,
4554
+ onAutoFit: handleAutoFit,
4364
4555
  ...c
4365
4556
  }
4366
4557
  )