UNPKG

@sap-ux/ui-components

Version:

SAP UI Components Library

249 lines 12.7 kB
import React, { useEffect, useRef, useState } from 'react'; import { List } from 'react-movable'; import { UIDefaultButton, UILoader } from '../index.js'; import { renderTitleRow, UIFlexibleTableRow } from './UIFlexibleTableRow.js'; import { UIFlexibleTableRowNoData } from './UIFlexibleTableRowNoData.js'; import { UIFlexibleTableLayout } from './types.js'; import './UIFlexibleTable.scss'; import { composeClassNames, getRowActionButtonId, getTableActionButtonId } from './utils.js'; import { RowActions } from './RowActions.js'; import { RowDataCells } from './RowData.js'; /** * UIFlexibleTable component. * * @exports * @param {UIFlexibleTableProps<T>} props * @returns {React.ReactElement} */ export function UIFlexibleTable(props) { const [currentFocusedRowIndex, setCurrentFocusedRowIndex] = useState(); const [currentFocusedRowAction, setCurrentFocusedRowAction] = useState(''); const [isInRowLayout, setIsInRowLayout] = useState(props.layout === UIFlexibleTableLayout.InlineFlex); const [titleRowRightPadding, setTitleRowRightPadding] = useState(0); const [rowToNavigate, setRowToNavigate] = useState(); const scrollableElement = useRef(null); const tableRootElementRef = useRef(null); const contentElementRef = useRef(null); const tableBodyElementRef = useRef(null); const scrollTargetRef = useRef(null); const onResize = (element) => { setIsInRowLayout(props.layout === UIFlexibleTableLayout.InlineFlex && isRowFitsContainer(props.inRowLayoutMinWidth, tableRootElementRef)); const rootContainer = tableRootElementRef.current; if (element && props.onContentSizeChange && rootContainer) { props.onContentSizeChange({ height: rootContainer.clientHeight, width: rootContainer.clientWidth }); } }; const onScrollBarStateChange = () => { const content = contentElementRef .current; const scrollableContent = scrollableElement.current; const scrollBarSize = content && scrollableContent ? content.clientWidth - scrollableContent.clientWidth : 0; setTitleRowRightPadding(scrollBarSize); }; const [resizeObserver] = useState(new ResizeObserver(onResize)); const scrollBarObserver = useRef(new ResizeObserver(onScrollBarStateChange)); useEffect(() => { if (tableRootElementRef.current) { resizeObserver.observe(tableRootElementRef.current); onResize(); } }, [tableRootElementRef.current]); useEffect(() => { if (currentFocusedRowIndex !== undefined) { restoreFocus(currentFocusedRowAction, currentFocusedRowIndex, props.id); } if (!props.isContentLoading && scrollTargetRef.current) { scrollTargetRef.current.scrollIntoView({ behavior: 'smooth', block: 'nearest' }); setRowToNavigate(undefined); } }, [props.isContentLoading, props.rows]); useEffect(() => { return () => { resizeObserver.disconnect(); }; }, []); useEffect(() => { onResize(); }, [props.layout, props.maxWidth]); const reorderTable = (oldIndex, newIndex) => { if (props.onTableReorder) { const result = props.onTableReorder({ oldIndex, newIndex }); if (typeof result === 'object' && result.isDropDisabled) { return; } setCurrentFocusedRowIndex(newIndex); } }; const addNewRow = () => { if (props.addRowButton?.onClick) { const result = props.addRowButton.onClick(); if (result instanceof Promise) { result.then((data) => setRowToNavigate(data?.scrollToRow)).catch(() => undefined); } else { setRowToNavigate(result?.scrollToRow); } } }; const onFocusRowAction = (rowIndex, actionName = '') => { setCurrentFocusedRowIndex(rowIndex); setCurrentFocusedRowAction(actionName); }; const renderRowActions = (rowIndex) => { return (React.createElement(RowActions, { rowIndex: rowIndex, tableProps: props, onFocusRowAction: (actionName) => onFocusRowAction(rowIndex, actionName), onMoveDownClick: () => reorderTable(rowIndex, rowIndex + 1), onMoveUpClick: () => reorderTable(rowIndex, rowIndex - 1) })); }; const renderRowData = (params) => (React.createElement(RowDataCells, { key: params.index ?? 0, isInRowLayout: isInRowLayout, row: params.value, rowIndex: params.index ?? 0, tableProps: props })); /** * Renders row. * * @param {NodeDragAndDropSortingParams} params * @returns {React.ReactElement<UIFlexibleTableRowProps<T>, 'UIFlexibleTableRow'>} */ function renderRow(params) { const rowIndex = params.index; const { isDropWarning } = props.onRenderRowContainer ? props.onRenderRowContainer({ readonly: !!props.readonly, rowIndex, isDragged: params.isDragged }) : { isDropWarning: false }; return (React.createElement(UIFlexibleTableRow, { key: `row-${rowIndex}`, dragAndDropParams: params, rowActions: renderRowActions(rowIndex ?? 0), rowData: renderRowData(params), tableProps: props, rowRef: typeof rowToNavigate === 'number' && rowIndex === rowToNavigate ? scrollTargetRef : undefined, className: isDropWarning ? 'highlight-drop-warning' : '' })); } const renderTable = (params) => { const ulElement = params.props.ref?.current; let { children } = params; if (ulElement && scrollableElement.current !== ulElement) { scrollableElement.current = ulElement; scrollBarObserver.current.observe(ulElement); } if (props.rows.length === 0 && props.noDataText) { children = (React.createElement(UIFlexibleTableRowNoData, { noRowBackground: props.noRowBackground, reverseBackground: props.reverseBackground }, props.noDataText)); } const getCursorStyle = () => { let cursorIcon = 'default'; if (props.onTableReorder) { cursorIcon = params.isDragged ? 'grabbing' : 'grab'; } return cursorIcon; }; return (React.createElement("ul", { ref: params.props.ref, className: `flexible-table-content-table${params.isDragged ? ' dragged' : ''}`, style: { cursor: getCursorStyle(), maxHeight: props.maxScrollableContentHeight ? `${props.maxScrollableContentHeight}px` : undefined } }, children)); }; const tableBody = getTableBody(props, renderTable, renderRow, reorderTable, tableBodyElementRef); const tableClasses = composeClassNames('flexible-table', [ props.layout === UIFlexibleTableLayout.InlineFlex ? 'inline-layout' : 'wrapping-layout', props.readonly ? 'readonly' : '' ]); const showTitleRow = props.showColumnTitles && isInRowLayout && !props.showColumnTitlesInCells; const tableRootElementStyle = { maxWidth: props.maxWidth ? `${props.maxWidth}px` : '100%' }; const getCustomTableActions = (actionsGenerator) => { if (actionsGenerator) { const customActions = actionsGenerator({ readonly: !!props.readonly }); return customActions.map((actionElement) => (React.createElement(React.Fragment, { key: `table-action-${actionElement.key}` }, React.createElement("div", { className: "flexible-table-header-action" }, actionElement)))); } return []; }; const primaryTableActions = getCustomTableActions(props.onRenderPrimaryTableActions); const secondaryTableActions = getCustomTableActions(props.onRenderSecondaryTableActions); const isEmptyHeader = !props.addRowButton && primaryTableActions.length + secondaryTableActions.length === 0; const contentClasses = composeClassNames('flexible-table-content', [ props.rows.length === 0 && !props.noDataText ? 'empty-table' : '', props.isContentLoading ? 'loading' : '', isEmptyHeader ? 'empty-table-header' : '' ]); return (React.createElement("div", { className: tableClasses, ref: tableRootElementRef, id: props.id, style: tableRootElementStyle, onBlur: () => { onFocusRowAction(); } }, isEmptyHeader ? (React.createElement(React.Fragment, null)) : (React.createElement("div", { className: "flexible-table-header" }, React.createElement("div", { className: "flexible-table-header-primary-actions" }, props.addRowButton && (React.createElement("div", { className: "flexible-table-header-action" }, React.createElement(UIDefaultButton, { iconProps: { iconName: 'Add' }, className: "flexible-table-btn-add", id: getTableActionButtonId(props.id, 'add-row'), primary: true, disabled: props.isAddItemDisabled || props.isContentLoading || props.readonly, onClick: addNewRow, title: props.addRowButton.title, "aria-label": props.addRowButton.ariaLabel ?? props.addRowButton.label }, props.addRowButton.label))), primaryTableActions), React.createElement("div", { className: "flexible-table-header-secondary-actions" }, secondaryTableActions))), React.createElement("div", { className: contentClasses, ref: contentElementRef }, showTitleRow && renderTitleRow(props, titleRowRightPadding), tableBody, props.isContentLoading && (React.createElement(UILoader, { className: 'uiLoaderXLarge flexible-table-overlay-loader', blockDOM: true }))))); } /** * Gets table body. * * @param {UIFlexibleTableProps<T>} props * @param {(params: { children: React.ReactNode; isDragged: boolean; props: { ref?: React.RefObject<any>; }; }) => React.ReactNode} renderTable * @param {( params: NodeDragAndDropSortingParams) => React.ReactElement<UIFlexibleTableRowProps<T>, 'UIFlexibleTableRow'>} renderRow * @param { (oldIndex: number, newIndex: number) => void} reorderTable * @param {React.MutableRefObject<any>} tableBodyElementRef * @returns {React.ReactNode} */ function getTableBody(props, renderTable, renderRow, reorderTable, tableBodyElementRef) { let tableBody; const { rows } = props.onBeforeTableRender ? props.onBeforeTableRender({ rows: props.rows }) : { rows: props.rows }; if (rows.length > 0 && props.onTableReorder && !props.readonly) { tableBody = (React.createElement(List, { values: rows, lockVertically: props.lockVertically, onChange: (params) => reorderTable(params.oldIndex, params.newIndex), renderList: (params) => renderTable(params), renderItem: (params) => renderRow(params), removableByMove: true, beforeDrag: props.onStartDragging })); } else { const children = props.rows.map((row, index) => { const rowProps = { key: index }; const dragAndDropParams = { index, isDragged: false, isOutOfBounds: false, isSelected: false, props: rowProps, value: row }; return renderRow(dragAndDropParams); }); tableBody = renderTable({ children, isDragged: false, props: { ...props, ref: tableBodyElementRef } }); } return tableBody; } /** * Restores focus. * * @param {string} currentFocusedRowAction * @param {number | undefined} currentFocusedRowIndex * @param {string} tableId */ function restoreFocus(currentFocusedRowAction, currentFocusedRowIndex, tableId) { if (!currentFocusedRowAction) { return; } let actionElement = document.getElementById(getRowActionButtonId(tableId, currentFocusedRowIndex, currentFocusedRowAction)); if (!actionElement) { return; } if (!actionElement.hasAttribute('disabled')) { actionElement.focus(); return; } const fallbackAction = currentFocusedRowAction === 'up' ? 'down' : 'up'; actionElement = document.getElementById(getRowActionButtonId(tableId, currentFocusedRowIndex, fallbackAction)); actionElement?.focus(); } /** * Check row fit container. * * @param {number | undefined} propThreshold * @param {React.MutableRefObject<HTMLDivElement | null>} containerRef * @returns {boolean} */ function isRowFitsContainer(propThreshold, containerRef) { let result = true; const inRowMinWidth = propThreshold ?? 600; const containerWidth = containerRef?.current?.clientWidth ?? 0; if (containerWidth) { result = containerWidth >= inRowMinWidth; } return result; } //# sourceMappingURL=UIFlexibleTable.js.map