@carbon/ibm-products
Version:
Carbon for IBM Products
546 lines (544 loc) • 18.4 kB
JavaScript
/**
* Copyright IBM Corp. 2020, 2026
*
* This source code is licensed under the Apache-2.0 license found in the
* LICENSE file in the root directory of this source tree.
*/
import { __toESM } from "../../_virtual/_rolldown/runtime.js";
import { require_classnames } from "../../node_modules/classnames/index.js";
import { pkg } from "../../settings.js";
import { usePreviousValue } from "../../global/js/hooks/usePreviousValue.js";
import { prepareProps } from "../../global/js/utils/props-helper.js";
import { deepCloneObject } from "../../global/js/utils/deepCloneObject.js";
import { removeCellSelections } from "./utils/removeCellSelections.js";
import { useSpreadsheetMouseUp } from "./hooks/useSpreadsheetMouseUp.js";
import { createCellSelectionArea } from "./utils/createCellSelectionArea.js";
import { checkActiveHeaderCell } from "./utils/checkActiveHeaderCell.js";
import { checkSelectedHeaderCell } from "./utils/checkSelectedHeaderCell.js";
import { getSpreadsheetWidth } from "./utils/getSpreadsheetWidth.js";
import { handleBodyCellClick, handleBodyCellHover, handleRowHeaderClick } from "./utils/commonEventHandlers.js";
import React, { forwardRef, useCallback, useEffect, useRef, useState } from "react";
import PropTypes from "prop-types";
import { px } from "@carbon/layout";
import { FixedSizeList } from "react-window";
//#region src/components/DataSpreadsheet/DataSpreadsheetBody.tsx
/**
* Copyright IBM Corp. 2022, 2022
*
* This source code is licensed under the Apache-2.0 license found in the
* LICENSE file in the root directory of this source tree.
*/
var import_classnames = /* @__PURE__ */ __toESM(require_classnames());
const blockClass = `${pkg.prefix}--data-spreadsheet`;
const DataSpreadsheetBody = forwardRef(({ activeCellRef, columns, activeCellCoordinates, defaultColumn, defaultEmptyRowCount, getTableBodyProps, headerGroups, setCurrentColumns, id, onDataUpdate, renderRowHeader, renderRowHeaderDirection, hasCustomRowHeader, prepareRow, rows, selectionAreaData, setSelectionAreaData, setActiveCellCoordinates, selectedHeaderReorderActive, setSelectedHeaderReorderActive, selectionAreas, setContainerHasFocus, setSelectionAreas, scrollBarSize, totalColumnsWidth, clickAndHoldActive, setClickAndHoldActive, currentMatcher, setCurrentMatcher, onSelectionAreaChange, setActiveCellInsideSelectionArea, totalVisibleColumns, setHeaderCellHoldActive, setColumnOrder, visibleColumns }, ref) => {
const [validStartingPoint, setValidStartingPoint] = useState(false);
const contentScrollRef = useRef(void 0);
const previousState = usePreviousValue({
selectionAreaData,
clickAndHoldActive,
rowHeight: defaultColumn?.rowHeight
}) || {};
useEffect(() => {
ref?.current.style.setProperty(`--${blockClass}--total-width`, px((totalColumnsWidth || 0) + (scrollBarSize || 0)));
}, [
ref,
scrollBarSize,
totalColumnsWidth
]);
useEffect(() => {
if (selectionAreaData?.length) {
let selectionChanged = false;
if (previousState?.selectionAreaData?.length !== selectionAreaData?.length || selectionAreaData?.[0]?.cells.length !== previousState?.selectionAreaData?.[0]?.cells.length) selectionChanged = true;
if (!clickAndHoldActive && previousState?.clickAndHoldActive || selectionChanged) onSelectionAreaChange?.(selectionAreaData);
}
}, [
previousState?.selectionAreaData,
selectionAreaData,
onSelectionAreaChange,
clickAndHoldActive,
previousState?.clickAndHoldActive
]);
useEffect(() => {
if (selectionAreas && selectionAreas.length) selectionAreas.map((area) => {
if (area.areaCreated) {
const rowStart = Math.min(area.point1.row, area.point2.row);
const rowEnd = Math.max(area.point1.row, area.point2.row);
const columnStart = Math.min(area.point1.column, area.point2.column);
const columnEnd = Math.max(area.point1.column, area.point2.column);
const selectionData = {
rows: {
start: rowStart,
end: rowEnd
},
columns: {
start: columnStart,
end: columnEnd
},
cells: populateSelectionAreaCellData({
rowStart,
rowEnd,
columnStart,
columnEnd
}),
selectionId: area.matcher
};
setSelectionAreaData?.((prev) => {
return [...deepCloneObject(prev).filter((item) => item.selectionId !== area.matcher), selectionData];
});
}
if (!area.areaCreated && area.point1 && area.point2 && area.matcher) createCellSelectionArea({
ref,
area,
blockClass,
defaultColumn,
selectionAreas,
setSelectionAreas,
setActiveCellInsideSelectionArea,
visibleColumns
});
});
}, [
selectionAreas,
setSelectionAreas,
defaultColumn,
onSelectionAreaChange,
setSelectionAreaData,
ref,
activeCellCoordinates,
setActiveCellInsideSelectionArea,
visibleColumns,
hasCustomRowHeader
]);
const populateSelectionAreaCellData = ({ rowStart, rowEnd, columnStart, columnEnd }) => {
const cellContainer = [];
for (let rowIndex = rowStart; rowIndex <= rowEnd; rowIndex++) for (let columnIndex = columnStart; columnIndex <= columnEnd; columnIndex++) cellContainer.push([
rowIndex,
columnIndex,
`${blockClass}__cell--${rowIndex}--${columnIndex}`
]);
return cellContainer;
};
useSpreadsheetMouseUp({
currentMatcher,
setClickAndHoldActive,
setSelectionAreas,
setValidStartingPoint,
selectedHeaderReorderActive,
setSelectedHeaderReorderActive,
validStartingPoint,
ref,
setHeaderCellHoldActive,
setColumnOrder,
visibleColumns,
setActiveCellCoordinates,
rows,
activeCellCoordinates,
defaultColumn,
selectionAreas
});
useEffect(() => {
let listContainer;
let activeCellButton;
if (spreadsheetBodyRef?.current) {
listContainer = spreadsheetBodyRef?.current;
activeCellButton = listContainer?.querySelector(`.${blockClass}__active-cell--highlight`);
}
if (activeCellButton && defaultColumn?.rowHeight !== previousState.rowHeight) {
activeCellButton.style.height = `${defaultColumn?.rowHeight}px`;
if (activeCellCoordinates) {
const activeTargetElement = ref?.current.querySelector(`[data-row-index="${activeCellCoordinates.row}"][data-column-index="${activeCellCoordinates.column}"]`);
const listContainer = ref?.current.querySelector(`.${blockClass}__list--container`);
let newActiveCellTopPosition;
if (activeTargetElement && listContainer) newActiveCellTopPosition = activeTargetElement?.getBoundingClientRect().top - listContainer.getBoundingClientRect().top;
activeCellButton.style.top = px(newActiveCellTopPosition);
removeCellSelections({
matcher: void 0,
spreadsheetRef: ref
});
selectionAreas?.map((area) => {
if (!area.areaCreated && area.point1 && area.point2 && area.matcher) return createCellSelectionArea({
ref,
area,
blockClass,
defaultColumn,
selectionAreas,
setSelectionAreas,
setActiveCellInsideSelectionArea,
visibleColumns
});
});
}
}
}, [
defaultColumn,
ref,
activeCellCoordinates,
previousState?.rowHeight,
selectionAreas,
setActiveCellInsideSelectionArea,
setSelectionAreas,
visibleColumns,
hasCustomRowHeader
]);
const removeDuplicateSelections = useCallback(() => {
const uniqueAttrArray = [], removedSelectionAreaMatcherArr = [];
ref?.current.querySelectorAll(`.${blockClass}__selection-area--element`).forEach((selectorEl) => {
const { top, left, height, width } = selectorEl.style;
const uniqueAttrStr = `${top}${left}${height}${width}`;
if (uniqueAttrArray.indexOf(uniqueAttrStr) == -1) uniqueAttrArray.push(uniqueAttrStr);
else {
selectorEl.remove();
removedSelectionAreaMatcherArr.push(selectorEl.getAttribute("data-matcher-id"));
}
});
if (removedSelectionAreaMatcherArr.length) {
setSelectionAreas?.((prev) => {
return deepCloneObject(prev).filter((item) => !removedSelectionAreaMatcherArr.includes(item.matcher));
});
setSelectionAreaData?.((prev) => {
return deepCloneObject(prev).filter((item) => !removedSelectionAreaMatcherArr.includes(item.selectionId));
});
}
}, [
ref,
setSelectionAreas,
setSelectionAreaData
]);
useEffect(() => {
removeDuplicateSelections();
}, [selectionAreas, removeDuplicateSelections]);
const handleBodyCellClickEvent = useCallback((cell, columnIndex) => {
return (event) => {
handleBodyCellClick(cell, columnIndex, event, currentMatcher, activeCellCoordinates, selectionAreas, setActiveCellCoordinates, setSelectionAreas, setContainerHasFocus, setClickAndHoldActive, setCurrentMatcher, ref, setSelectionAreaData, setActiveCellInsideSelectionArea, activeCellRef, setValidStartingPoint);
};
}, [
currentMatcher,
activeCellCoordinates,
selectionAreas,
setActiveCellCoordinates,
setSelectionAreas,
setContainerHasFocus,
setClickAndHoldActive,
setCurrentMatcher,
ref,
setSelectionAreaData,
setActiveCellInsideSelectionArea,
activeCellRef
]);
const handleBodyScroll = () => {
const headerRowElement = ref.current.querySelector(`
.${blockClass}__header--container .${blockClass}__tr`) || new HTMLDivElement();
headerRowElement.scrollLeft = contentScrollRef?.current.scrollLeft;
};
useEffect(() => {
contentScrollRef.current.addEventListener("scroll", () => handleBodyScroll());
const contentScrollElementRef = contentScrollRef.current || new HTMLElement();
return () => {
contentScrollElementRef.removeEventListener("scroll", handleBodyScroll);
};
});
const handleBodyCellHoverEvent = useCallback((cell, columnIndex) => {
return () => {
handleBodyCellHover(cell, columnIndex, clickAndHoldActive, currentMatcher, setSelectionAreas);
};
}, [
clickAndHoldActive,
currentMatcher,
setSelectionAreas
]);
const handleRowHeaderClickEvent = useCallback((index) => {
return (event) => {
handleRowHeaderClick(index, event, columns, ref, setSelectionAreas, setCurrentMatcher, setActiveCellCoordinates, activeCellCoordinates, rows, setSelectionAreaData);
};
}, [
columns,
ref,
setSelectionAreas,
setCurrentMatcher,
setActiveCellCoordinates,
activeCellCoordinates,
rows,
setSelectionAreaData
]);
useEffect(() => {
if (!rows?.length) {
const buildEmptyRows = () => {
const emptyRowData = [];
[...Array(defaultEmptyRowCount)].map(() => {
const emptyCell = {};
headerGroups?.[0]?.headers.map((header) => {
emptyCell[header.id] = null;
});
emptyRowData.push(emptyCell);
});
onDataUpdate?.(emptyRowData);
};
buildEmptyRows();
}
if (headerGroups?.[0] && typeof setCurrentColumns === "function") {
const headers = headerGroups[0].headers;
setCurrentColumns(headers);
}
}, [
rows,
headerGroups,
defaultEmptyRowCount,
onDataUpdate,
setCurrentColumns
]);
const RenderEmptyRows = () => {
return /* @__PURE__ */ React.createElement("div", null);
};
const RenderRow = useCallback(({ index, style }) => {
const row = rows?.[index];
if (rows && rows.length) {
prepareRow?.(row);
const rowProps = prepareProps(row.getRowProps({ style }), "key");
return /* @__PURE__ */ React.createElement("div", {
key: { ...row.getRowProps().key },
...rowProps,
className: (0, import_classnames.default)(`${blockClass}__tr`),
"data-row-index": index,
"aria-rowindex": index + 1,
"aria-owns": `${blockClass}__cell-editor-text-area`
}, /* @__PURE__ */ React.createElement("div", {
role: "rowheader",
className: `${blockClass}__td-th--cell-container`
}, /* @__PURE__ */ React.createElement("button", {
id: `${blockClass}__cell--${index}--header`,
tabIndex: -1,
"data-row-index": index,
"data-column-index": "header",
type: "button",
onClick: handleRowHeaderClickEvent(index),
className: (0, import_classnames.default)(`${blockClass}__td`, `${blockClass}__td-th`, `${blockClass}--interactive-cell-element`, {
[`${blockClass}__td_custom`]: hasCustomRowHeader ? true : false,
[`${blockClass}__td-th--active-header`]: !hasCustomRowHeader && (activeCellCoordinates?.row === index || checkActiveHeaderCell(index, selectionAreas, "row")),
[`${blockClass}__td-th--selected-header`]: !hasCustomRowHeader && checkSelectedHeaderCell(index, selectionAreas, "row", columns)
}),
style: {
width: defaultColumn?.rowHeaderWidth,
flexDirection: hasCustomRowHeader ? renderRowHeaderDirection === "Left" ? "row-reverse" : row : void 0
}
}, index + 1, hasCustomRowHeader && typeof renderRowHeader === "function" && renderRowHeader(index))), row.cells.map((cell, index) => {
const cellProps = prepareProps(cell.getCellProps(), "key");
return /* @__PURE__ */ React.createElement("div", {
key: `cell_${index}`,
"aria-colindex": index + 1,
...cellProps,
role: "gridcell",
style: {
...cell.getCellProps().style,
display: "grid",
minWidth: cell?.column?.width || defaultColumn?.width
}
}, /* @__PURE__ */ React.createElement("button", {
id: `${blockClass}__cell--${cell.row.index}--${index}`,
tabIndex: -1,
"data-row-index": cell.row.index,
"data-column-index": index,
className: (0, import_classnames.default)(`${blockClass}__td`, `${blockClass}__body--td`, `${blockClass}--interactive-cell-element`),
onMouseDown: handleBodyCellClickEvent(cell, index),
onMouseOver: handleBodyCellHoverEvent(cell, index),
onFocus: () => {},
type: "button"
}, cell.render("Cell")));
}));
}
}, [
prepareRow,
renderRowHeader,
rows,
hasCustomRowHeader,
activeCellCoordinates?.row,
selectionAreas,
handleRowHeaderClickEvent,
handleBodyCellClickEvent,
handleBodyCellHoverEvent,
columns,
defaultColumn,
renderRowHeaderDirection
]);
const spreadsheetBodyRef = useRef(void 0);
return /* @__PURE__ */ React.createElement("div", {
ref: spreadsheetBodyRef,
className: (0, import_classnames.default)(`${blockClass}__body--container`),
...getTableBodyProps?.()
}, /* @__PURE__ */ React.createElement(FixedSizeList, {
className: (0, import_classnames.default)(`${blockClass}__list--container`, `${blockClass}__list--container--${id}`),
height: 400,
itemCount: rows?.length || defaultEmptyRowCount,
itemSize: defaultColumn?.rowHeight,
width: getSpreadsheetWidth({
headerGroup: void 0,
type: void 0,
scrollBarSizeValue: scrollBarSize,
totalVisibleColumns,
defaultColumn,
totalColumnsWidth,
visibleColumns
}),
outerRef: contentScrollRef
}, rows?.length ? RenderRow : RenderEmptyRows));
});
DataSpreadsheetBody.propTypes = {
/**
* Object containing the active cell coordinates
*/
/**@ts-ignore */
activeCellCoordinates: PropTypes.shape({
row: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
column: PropTypes.oneOfType([PropTypes.string, PropTypes.number])
}),
/**
*This is the ref of the button input, which is the active cell element
*/
/**@ts-ignore */
activeCellRef: PropTypes.object,
/**
* Is the user clicking and holding in the data spreadsheet body
*/
clickAndHoldActive: PropTypes.bool,
/**
* All of the spreadsheet columns
*/
columns: PropTypes.array,
/**
* This represents the id of the current cell selection area
*/
currentMatcher: PropTypes.string,
/**
* Default spreadsheet sizing values
*/
/**@ts-ignore */
defaultColumn: PropTypes.shape({
rowHeight: PropTypes.number,
rowHeaderWidth: PropTypes.number,
width: PropTypes.number
}),
/**
* Sets the number of empty rows to be created when there is no data provided
*/
defaultEmptyRowCount: PropTypes.number,
/**
* Function to set table body prop values
*/
/**@ts-ignore */
getTableBodyProps: PropTypes.func,
/**
* Check if spreadsheet is using custom row header component attached
*/
hasCustomRowHeader: PropTypes.bool,
/**
* Headers provided from useTable hook
*/
headerGroups: PropTypes.arrayOf(PropTypes.object),
/**
* The spreadsheet id
*/
id: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
/**
* The event handler that is called when the active cell changes
*/
onActiveCellChange: PropTypes.func,
/**
* The event handler that is called to set the rows for the empty spreadsheet
*/
onDataUpdate: PropTypes.func,
/**
* The event handler that is called when the selection areas change
*/
onSelectionAreaChange: PropTypes.func,
/**
* Prepare row function from react-table
*/
prepareRow: PropTypes.func,
/**
* Component next to numbering rows
*/
renderRowHeader: PropTypes.func,
/**
* Component next to numbering rows
*/
renderRowHeaderDirection: PropTypes.string,
/**
* All of the spreadsheet row data
*/
rows: PropTypes.arrayOf(PropTypes.object),
/**
* The scrollbar width
*/
scrollBarSize: PropTypes.number,
/**
* Header reordering is active
*/
selectedHeaderReorderActive: PropTypes.bool,
/**
* Array of selection area data
*/
selectionAreaData: PropTypes.array,
/**
* Array of selection areas
*/
selectionAreas: PropTypes.array,
/**
* Setter fn for activeCellCoordinates state value
*/
setActiveCellCoordinates: PropTypes.func,
/**
* Setter fn for active cell inside of selection area
*/
setActiveCellInsideSelectionArea: PropTypes.func,
/**
* Setter fn for clickAndHold state value
*/
setClickAndHoldActive: PropTypes.func,
/**
* Setter fn for column ordering, provided from react-table
*/
setColumnOrder: PropTypes.func,
/**
* Setter fn for containerHasFocus state value
*/
setContainerHasFocus: PropTypes.func,
/**
* Set current columns after drag drop
*/
setCurrentColumns: PropTypes.func,
/**
* Setter fn for currentMatcher state value
*/
setCurrentMatcher: PropTypes.func,
/**
* Setter fn for header cell hold active value
*/
setHeaderCellHoldActive: PropTypes.func,
/**
* Set header reordering active or not
*/
setSelectedHeaderReorderActive: PropTypes.func,
/**
* Setter fn for selectionAreaData state value
*/
setSelectionAreaData: PropTypes.func,
/**
* Setter fn for selectionAreas state value
*/
setSelectionAreas: PropTypes.func,
/**
* The total columns width
*/
totalColumnsWidth: PropTypes.number,
/**
* The total number of columns to be initially visible, additional columns will be rendered and
* visible via horizontal scrollbar
*/
totalVisibleColumns: PropTypes.number,
/**
* Prop from react-table used to reorder columns
*/
/**@ts-ignore */
visibleColumns: PropTypes.array
};
//#endregion
export { DataSpreadsheetBody };