@carbon/react
Version:
React components for the Carbon Design System
183 lines (177 loc) • 7.14 kB
JavaScript
/**
* Copyright IBM Corp. 2016, 2023
*
* 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 { extends as _extends } from '../../_virtual/_rollupPluginBabelHelpers.js';
import React, { useContext, useState, useRef, useCallback } from 'react';
import PropTypes from 'prop-types';
import cx from 'classnames';
import { usePrefix } from '../../internal/usePrefix.js';
import { TableContext } from './TableContext.js';
import { useWindowEvent } from '../../internal/useEvent.js';
import useIsomorphicEffect from '../../internal/useIsomorphicEffect.js';
import { debounce } from '../../node_modules/es-toolkit/dist/compat/function/debounce.js';
const isElementWrappingContent = (element, context) => {
if (element.children.length > 0) {
return false;
}
const computedStyles = window.getComputedStyle(element);
context.font = computedStyles.font ? computedStyles.font : `${computedStyles.fontSize}" "${computedStyles.fontFamily}`;
const measuredText = context?.measureText(element.textContent ?? '');
let textWidth = measuredText.width ?? 0;
// account for letter spacing
const letterSpacing = computedStyles.letterSpacing?.split('px');
if (letterSpacing && letterSpacing.length && !isNaN(Number(letterSpacing[0]))) {
textWidth += Number(letterSpacing[0]) * (element.textContent?.length ?? 0);
}
// account for padding
const paddingLeft = computedStyles.paddingLeft?.split('px');
if (paddingLeft && paddingLeft.length && !isNaN(Number(paddingLeft[0]))) {
textWidth += Number(paddingLeft[0]);
}
const paddingRight = computedStyles.paddingLeft?.split('px');
if (paddingRight && paddingRight.length && !isNaN(Number(paddingRight[0]))) {
textWidth += Number(paddingRight[0]);
}
// if measured textWidth is larger than the cell's width, then the content is being wrapped
if (textWidth > element.getBoundingClientRect().width) {
return true;
}
return false;
};
const Table = ({
className,
children,
useZebraStyles,
size = 'lg',
isSortable = false,
useStaticWidth,
stickyHeader,
overflowMenuOnHover = true,
experimentalAutoAlign = false,
tabIndex,
...other
}) => {
const {
titleId,
descriptionId
} = useContext(TableContext);
const prefix = usePrefix();
const [isScrollable, setIsScrollable] = useState(false);
const tableRef = useRef(null);
const componentClass = cx(`${prefix}--data-table`, className, {
[`${prefix}--data-table--${size}`]: size,
[`${prefix}--data-table--sort`]: isSortable,
[`${prefix}--data-table--zebra`]: useZebraStyles,
[`${prefix}--data-table--static`]: useStaticWidth,
[`${prefix}--data-table--sticky-header`]: stickyHeader,
[`${prefix}--data-table--visible-overflow-menu`]: !overflowMenuOnHover
});
const toggleTableBodyAlignmentClass = useCallback((alignTop = false) => {
alignTop ? tableRef.current?.classList.add(`${prefix}--data-table--top-aligned-body`) : tableRef.current?.classList.remove(`${prefix}--data-table--top-aligned-body`);
}, [prefix]);
const toggleTableHeaderAlignmentClass = useCallback((alignTop = false) => {
alignTop ? tableRef.current?.classList.add(`${prefix}--data-table--top-aligned-header`) : tableRef.current?.classList.remove(`${prefix}--data-table--top-aligned-header`);
}, [prefix]);
const setTableAlignment = useCallback(() => {
if (experimentalAutoAlign) {
const canvas = document.createElement('canvas');
const context = canvas.getContext('2d');
if (tableRef.current && context) {
const isBodyMultiline = Array.from(tableRef.current.querySelectorAll('td')).some(td => isElementWrappingContent(td, context));
const isHeaderMultiline = Array.from(tableRef.current.querySelectorAll('th')).some(th => {
const label = th.querySelector(`.${prefix}--table-header-label`);
return label && isElementWrappingContent(label, context);
});
toggleTableBodyAlignmentClass(isBodyMultiline);
toggleTableHeaderAlignmentClass(isHeaderMultiline);
}
} else {
toggleTableBodyAlignmentClass(false);
toggleTableHeaderAlignmentClass(false);
}
}, [experimentalAutoAlign, toggleTableBodyAlignmentClass, toggleTableHeaderAlignmentClass, prefix]);
const debouncedSetTableAlignment = debounce(setTableAlignment, 100);
useWindowEvent('resize', debouncedSetTableAlignment);
// Used to set a tabIndex when the Table is horizontally scrollable
const setTabIndex = useCallback(() => {
const tableContainer = tableRef?.current?.parentNode;
const tableHeader = tableRef?.current?.firstChild;
if (tableHeader?.scrollWidth > tableContainer?.clientWidth) {
setIsScrollable(true);
} else {
setIsScrollable(false);
}
}, []);
const debouncedSetTabIndex = debounce(setTabIndex, 100);
useWindowEvent('resize', debouncedSetTabIndex);
useIsomorphicEffect(() => {
setTabIndex();
}, [setTabIndex]);
// recalculate table alignment once fonts have loaded
if (typeof document !== 'undefined' && document?.fonts?.status && document.fonts.status !== 'loaded') {
document.fonts.ready.then(() => {
setTableAlignment();
});
}
useIsomorphicEffect(() => {
setTableAlignment();
}, [setTableAlignment, size]);
const table = /*#__PURE__*/React.createElement("div", {
className: `${prefix}--data-table-content`
// eslint-disable-next-line jsx-a11y/no-noninteractive-tabindex
,
tabIndex: tabIndex ?? (isScrollable ? 0 : undefined)
}, /*#__PURE__*/React.createElement("table", _extends({
"aria-labelledby": titleId,
"aria-describedby": descriptionId
}, other, {
className: componentClass,
ref: tableRef
}), children));
return stickyHeader ? /*#__PURE__*/React.createElement("section", {
className: `${prefix}--data-table_inner-container`
}, table) : table;
};
Table.propTypes = {
/**
* Pass in the children that will be rendered within the Table
*/
children: PropTypes.node,
className: PropTypes.string,
/**
* Experimental property. Allows table to align cell contents to the top if there is text wrapping in the content. Might have performance issues, intended for smaller tables
*/
experimentalAutoAlign: PropTypes.bool,
/**
* `false` If true, will apply sorting styles
*/
isSortable: PropTypes.bool,
/**
* Specify whether the overflow menu (if it exists) should be shown always, or only on hover
*/
overflowMenuOnHover: PropTypes.bool,
/**
* Change the row height of table. Currently supports `xs`, `sm`, `md`, `lg`, and `xl`.
*/
size: PropTypes.oneOf(['xs', 'sm', 'md', 'lg', 'xl']),
/**
* `false` If true, will keep the header sticky (only data rows will scroll)
*/
stickyHeader: PropTypes.bool,
/**
* `false` If true, will use a width of 'auto' instead of 100%
*/
useStaticWidth: PropTypes.bool,
/**
* `true` to add useZebraStyles striping.
*/
useZebraStyles: PropTypes.bool,
/**
* Specify the table tabIndex
*/
tabIndex: PropTypes.number
};
export { Table, Table as default };