@yamada-ui/react
Version:
React UI components of the Yamada, by the Yamada, for the Yamada built with React and Emotion
554 lines (550 loc) • 21.9 kB
JavaScript
"use client";
import { runKeyAction } from "../../utils/dom.js";
import { useUpdateEffect } from "../../utils/effect.js";
import { utils_exports } from "../../utils/index.js";
import { styled } from "../../core/system/factory.js";
import { focusRingStyle } from "../../core/css/focus-ring.js";
import { mergeProps } from "../../core/components/props.js";
import { createComponent } from "../../core/components/create-component.js";
import { ChevronUpIcon } from "../icon/icons/chevron-up-icon.js";
import { ChevronsUpDownIcon } from "../icon/icons/chevrons-up-down-icon.js";
import { useControllableState } from "../../hooks/use-controllable-state/index.js";
import { useI18n } from "../../providers/i18n-provider/i18n-provider.js";
import { Checkbox } from "../checkbox/checkbox.js";
import { NativeTableRoot, Tbody, Td, Tfoot, Th, Thead, Tr } from "../native-table/native-table.js";
import { useMemo, useRef } from "react";
import { jsx, jsxs } from "react/jsx-runtime";
import { createColumnHelper, flexRender, getCoreRowModel, getFilteredRowModel, getPaginationRowModel, getSortedRowModel, useReactTable } from "@tanstack/react-table";
//#region src/components/table/table.tsx
function getMergeHeaderGroups(headerGroups) {
if (headerGroups.length <= 1) return headerGroups;
const columnsIds = /* @__PURE__ */ new Set();
return headerGroups.map((headerGroup, depth, { length: fullDepth }) => {
return {
...headerGroup,
headers: headerGroup.headers.filter((header) => !columnsIds.has(header.column.id)).map((header) => {
columnsIds.add(header.column.id);
return header.isPlaceholder ? {
...header,
isPlaceholder: false,
rowSpan: fullDepth - depth
} : {
...header,
rowSpan: 1
};
})
};
});
}
function getMergeFooterGroups(headerGroups) {
if (headerGroups.length <= 1) return headerGroups;
return headerGroups.map((headerGroup, depth) => {
const nextHeaderGroups = headerGroups.slice(depth + 1);
return {
...headerGroup,
headers: headerGroup.headers.filter((header) => !header.isPlaceholder).map((header) => {
if (nextHeaderGroups.length === 0) return header;
const rowSpan = nextHeaderGroups.reduce((acc, nextHeaderGroup) => {
return acc + (nextHeaderGroup.headers.some((nextHeader) => nextHeader.column.id === header.column.id) ? 1 : 0);
}, 1);
return {
...header,
rowSpan
};
})
};
});
}
const { ComponentContext, PropsContext: TablePropsContext, useComponentContext, usePropsContext: useTablePropsContext, withContext } = createComponent("table");
/**
* `Table` is a table component equipped with column sorting, row selection, and click event features.
*
* @see https://yamada-ui.com/docs/components/table
*/
const Table = withContext(({ colorScheme, size, variant, columnFilters: columnFiltersProp, columnResizeMode = "onChange", columns: columnsProp, data, defaultColumnFilters, defaultPagination = {
pageIndex: 0,
pageSize: 20
}, defaultRowSelection = {}, defaultSorting, enableAutoResizeTableWidth = false, enableColumnResizing = false, enableKeyboardNavigation = true, enablePagination = false, enableRowSelection = false, footer, header, highlightOnHover = !!enableRowSelection, highlightOnSelected = !!enableRowSelection, initialFocusableCell = {
colIndex: 0,
rowIndex: 0
}, layout, lineClamp, manualPagination, pagination: paginationProp, rowCount: totalRowCount, rowSelection: rowSelectionProp, selectOnClickRow = false, sortDescFirst = false, sorting: sortingProp, sortingIcon, state, stickyFooter, stickyHeader, striped, truncated, withBorder, withCheckbox = true, withColumnBorders, withFooterCheckbox = false, withFooterGroups = false, withScrollArea, cellProps, checkboxProps, footerGroupProps, footerProps, headerCheckboxProps, headerGroupProps, headerProps, resizableTriggerProps, rowCheckboxProps, rowProps, scrollAreaProps, sortingIconProps, tableProps, tbodyProps, tfootProps, theadProps, onColumnFiltersChange: onColumnFiltersChangeProp, onPaginationChange: onPaginationChangeProp, onRowClick, onRowDoubleClick, onRowSelectionChange: onRowSelectionChangeProp, onSortingChange: onSortingChangeProp,...rest }) => {
const { t } = useI18n("table");
const initialFocus = useRef(false);
const ref = useRef(null);
const focusedCell = useRef(null);
const [rowSelection, onRowSelectionChange] = useControllableState({
defaultValue: defaultRowSelection,
value: rowSelectionProp,
onChange: onRowSelectionChangeProp
});
const [sorting, onSortingChange] = useControllableState({
defaultValue: defaultSorting,
value: sortingProp,
onChange: onSortingChangeProp
});
const [pagination, onPaginationChange] = useControllableState({
defaultValue: defaultPagination,
value: paginationProp,
onChange: onPaginationChangeProp
});
const [columnFilters, onColumnFiltersChange] = useControllableState({
defaultValue: defaultColumnFilters,
value: columnFiltersProp,
onChange: onColumnFiltersChangeProp
});
const table = useReactTable({
columnResizeMode,
columns: useMemo(() => {
if (!enableRowSelection || !withCheckbox) return columnsProp;
const clonedColumns = [...columnsProp];
const header$1 = ({ header: header$2, table: table$1 }) => {
return /* @__PURE__ */ jsx(Checkbox, { ...mergeProps({
checked: table$1.getIsAllRowsSelected(),
indeterminate: table$1.getIsSomeRowsSelected(),
indicatorProps: { outline: "none" },
inputProps: {
"aria-label": t("Select all rows"),
"data-focusable": "",
tabIndex: -1
},
onChange: table$1.getToggleAllRowsSelectedHandler()
}, checkboxProps ?? {}, (0, utils_exports.runIfFn)(headerCheckboxProps, header$2) ?? {})() });
};
const cell = ({ row }) => {
return /* @__PURE__ */ jsx(Checkbox, { ...mergeProps({
checked: row.getIsSelected(),
disabled: !row.getCanSelect(),
indeterminate: row.getIsSomeSelected(),
indicatorProps: { outline: "none" },
inputProps: {
"aria-label": t("Select row"),
"data-focusable": "",
tabIndex: -1
},
onChange: row.getToggleSelectedHandler()
}, checkboxProps ?? {}, (0, utils_exports.runIfFn)(rowCheckboxProps, row) ?? {})() });
};
clonedColumns.unshift({
id: "select",
cell,
header: header$1,
...withFooterCheckbox ? { footer: header$1 } : { footerProps: { "aria-hidden": "true" } },
cellProps: { verticalAlign: "middle" },
headerProps: { w: "calc({spaces.4} + {space-x} * 2)" }
});
return clonedColumns;
}, [
checkboxProps,
columnsProp,
enableRowSelection,
headerCheckboxProps,
rowCheckboxProps,
t,
withCheckbox,
withFooterCheckbox
]),
data,
enableColumnResizing,
enableRowSelection,
getCoreRowModel: getCoreRowModel(),
getFilteredRowModel: getFilteredRowModel(),
getSortedRowModel: getSortedRowModel(),
manualPagination,
rowCount: totalRowCount,
sortDescFirst,
state: {
columnFilters,
pagination: enablePagination ? pagination : void 0,
rowSelection,
sorting,
...state
},
onColumnFiltersChange,
onPaginationChange,
onRowSelectionChange,
onSortingChange,
...enablePagination ? { getPaginationRowModel: getPaginationRowModel() } : {},
...rest
});
const headerGroups = table.getHeaderGroups();
const mergedHeaderGroups = getMergeHeaderGroups(headerGroups);
const rows = table.getRowModel().rows;
const footerGroups = table.getFooterGroups();
const mergedFooterGroups = getMergeFooterGroups(withFooterGroups ? footerGroups : []);
const headerGroupCount = headerGroups.length;
const rowCount = rows.length;
const colCount = table.getAllLeafColumns().length;
const maxColIndex = colCount - 1;
const pageIndex = enablePagination ? table.getState().pagination.pageIndex : 0;
const cellMap = useMemo(() => {
const cellMap$1 = /* @__PURE__ */ new Map();
if (!enableKeyboardNavigation) return cellMap$1;
const insertCellMap = (id, colSpan, rowSpan, colIndex, rowIndex) => {
for (let i = 0; i < colSpan; i++) cellMap$1.set(`${colIndex + i}-${rowIndex}`, `${colIndex}-${rowIndex}`);
for (let i = 1; i < rowSpan; i++) cellMap$1.set(`${colIndex}-${rowIndex + i}`, `${colIndex}-${rowIndex}`);
};
const insertCellMapByHeaderGroup = (headerGroup, rowIndex) => {
let placeholderCount = 0;
headerGroup.headers.forEach((header$1) => {
const colSpan = header$1.colSpan || 1;
const rowSpan = header$1.rowSpan || 1;
const colIndex = header$1.index + placeholderCount;
placeholderCount += colSpan - 1;
insertCellMap(header$1.id, colSpan, rowSpan, colIndex, rowIndex);
});
};
mergedHeaderGroups.forEach((headerGroup, rowIndex) => {
insertCellMapByHeaderGroup(headerGroup, rowIndex);
});
rows.forEach((row, rowIndex) => {
rowIndex += headerGroupCount;
row.getVisibleCells().forEach((cell) => {
const colIndex = cell.column.getIndex();
insertCellMap(cell.id, 1, 1, colIndex, rowIndex);
});
});
mergedFooterGroups.forEach((footerGroup, rowIndex) => {
rowIndex += headerGroupCount + rowCount;
insertCellMapByHeaderGroup(footerGroup, rowIndex);
});
return cellMap$1;
}, [
enableKeyboardNavigation,
headerGroupCount,
mergedFooterGroups,
mergedHeaderGroups,
rows,
rowCount
]);
const context = useMemo(() => ({
columnResizeMode,
sortingIcon,
table,
resizableTriggerProps,
sortingIconProps
}), [
columnResizeMode,
sortingIcon,
sortingIconProps,
resizableTriggerProps,
table
]);
const getCell = (evOrEl) => {
if (!evOrEl) return;
const el = evOrEl instanceof HTMLElement ? evOrEl.closest("th, td") : "target" in evOrEl && evOrEl.target instanceof HTMLElement ? evOrEl.target.closest("th, td") : null;
if (!(el instanceof HTMLTableCellElement)) return;
const { colindex, rowindex } = el.dataset;
const { colSpan, rowSpan } = el;
if (!colindex || !rowindex) return;
return {
colIndex: parseInt(colindex),
colSpan,
el,
rowIndex: parseInt(rowindex),
rowSpan
};
};
const getShouldFocusCell = (colIndex, rowIndex) => {
const [trulyColIndex, trulyRowIndex] = cellMap.get(`${colIndex}-${rowIndex}`)?.split("-") ?? [];
if (!trulyColIndex || !trulyRowIndex) return;
const targetEl = ref.current?.querySelector(`[data-colindex="${trulyColIndex}"][data-rowindex="${trulyRowIndex}"]`);
if (!targetEl || !(targetEl instanceof HTMLTableCellElement)) return;
return targetEl;
};
const removeTabIndex = (el) => {
if (!el || !(el instanceof HTMLElement)) return;
el.tabIndex = -1;
el.querySelectorAll("[data-focusable]").forEach((el$1) => {
if (el$1 instanceof HTMLElement) el$1.tabIndex = -1;
});
};
const onCellFocus = (el, colIndex, rowIndex) => {
const targetEl = getShouldFocusCell(colIndex, rowIndex);
if (!targetEl) return;
focusedCell.current = {
colIndex,
rowIndex
};
removeTabIndex(el);
const shouldFocusEl = targetEl.querySelector("[data-focusable]") ?? targetEl;
if (shouldFocusEl instanceof HTMLElement) {
shouldFocusEl.tabIndex = 0;
shouldFocusEl.focus();
}
};
const onFocus = (ev) => {
if (initialFocus.current) return;
initialFocus.current = true;
const cell = getCell(ev);
if (!cell) return;
onCellFocus(cell.el, cell.colIndex, cell.rowIndex);
};
const onKeyDown = (ev) => {
if (!enableKeyboardNavigation) return;
const cell = getCell(ev);
if (!cell) return;
runKeyAction(ev, {
ArrowDown: () => onCellFocus(cell.el, cell.colIndex, cell.rowIndex + cell.rowSpan),
ArrowLeft: () => onCellFocus(cell.el, cell.colIndex - 1, cell.rowIndex),
ArrowRight: () => onCellFocus(cell.el, cell.colIndex + cell.colSpan, cell.rowIndex),
ArrowUp: () => onCellFocus(cell.el, cell.colIndex, cell.rowIndex - 1),
End: () => onCellFocus(cell.el, maxColIndex, cell.rowIndex),
Home: () => onCellFocus(cell.el, 0, cell.rowIndex),
...enablePagination ? {
PageDown: () => {
if (!table.getCanNextPage()) return;
table.setPageIndex(pageIndex + 1);
},
PageUp: () => {
if (!table.getCanPreviousPage()) return;
table.setPageIndex(pageIndex - 1);
}
} : {}
});
};
const getTabIndex = (colIndex, rowIndex) => {
if (!enableKeyboardNavigation) return void 0;
return colIndex === initialFocusableCell.colIndex && rowIndex === initialFocusableCell.rowIndex ? 0 : void 0;
};
useUpdateEffect(() => {
if (!enableKeyboardNavigation) return;
const { colIndex, rowIndex } = focusedCell.current ?? initialFocusableCell;
const targetEl = getShouldFocusCell(colIndex, rowIndex);
if (targetEl) targetEl.tabIndex = 0;
}, [pageIndex, enableKeyboardNavigation]);
return /* @__PURE__ */ jsxs(ComponentContext, {
value: context,
children: [
header ? (0, utils_exports.runIfFn)(header, table) : null,
/* @__PURE__ */ jsxs(NativeTableRoot, {
...mergeProps({
ref,
style: enableColumnResizing && enableAutoResizeTableWidth ? { width: table.getCenterTotalSize() } : {},
colorScheme,
size,
variant,
"aria-colcount": colCount,
"aria-multiselectable": enableRowSelection ? "true" : void 0,
"aria-rowcount": totalRowCount || data.length,
highlightOnHover,
highlightOnSelected,
layout,
role: "grid",
stickyFooter,
stickyHeader,
striped,
withBorder,
withColumnBorders,
withScrollArea,
scrollAreaProps,
onFocus,
onKeyDown
}, tableProps ?? {})(),
children: [
/* @__PURE__ */ jsx(Thead, {
role: "rowgroup",
...theadProps,
children: mergedHeaderGroups.map((headerGroup, rowIndex) => {
let placeholderCount = 0;
return /* @__PURE__ */ jsx(Tr, {
"aria-rowindex": rowIndex + 1,
role: "row",
...(0, utils_exports.runIfFn)(headerGroupProps, headerGroup),
children: headerGroup.headers.map((header$1) => {
const { columnDef } = header$1.column;
const colIndex = header$1.index + placeholderCount;
const tabIndex = getTabIndex(colIndex, rowIndex);
const canSort = header$1.column.getCanSort();
const sorted = header$1.column.getIsSorted();
const canResize = header$1.column.getCanResize();
const resizing = header$1.column.getIsResizing();
const children = header$1.isPlaceholder ? null : flexRender(header$1.column.columnDef.header, header$1.getContext());
placeholderCount += (header$1.colSpan || 1) - 1;
return /* @__PURE__ */ jsxs(Th, {
"aria-colindex": colIndex + 1,
"aria-rowindex": rowIndex + 1,
"aria-sort": sorted ? sorted === "asc" ? "ascending" : "descending" : "none",
"data-colindex": colIndex,
"data-rowindex": rowIndex,
colSpan: header$1.colSpan || void 0,
numeric: columnDef.numeric,
pe: canSort ? "calc((1rem * {lineHeights.moderate}) + {space-x})" : void 0,
position: "relative",
role: "columnheader",
rowSpan: header$1.rowSpan || void 0,
tabIndex,
...mergeProps({
css: { "&:has([data-focusable]:focus-visible)": focusRingStyle.outline },
style: enableColumnResizing ? { width: header$1.getSize() } : {}
}, (0, utils_exports.runIfFn)(headerProps, header$1) ?? {}, columnDef.headerProps ?? {})(),
children: [
/* @__PURE__ */ jsx(TruncatedText, {
lineClamp: columnDef.lineClamp ?? lineClamp,
truncated: columnDef.truncated ?? truncated,
children
}),
canSort ? /* @__PURE__ */ jsx(SortingIcon, {
sorted,
onClick: header$1.column.getToggleSortingHandler()
}) : null,
canResize ? /* @__PURE__ */ jsx(ResizableTrigger, {
resizing,
onDoubleClick: header$1.column.resetSize,
onMouseDown: header$1.getResizeHandler(),
onTouchStart: header$1.getResizeHandler()
}) : null
]
}, header$1.id);
})
}, headerGroup.id);
})
}),
/* @__PURE__ */ jsx(Tbody, {
role: "rowgroup",
...tbodyProps,
children: rows.map((row, rowIndex) => {
rowIndex += headerGroupCount;
const selected = !!rowSelection[row.id];
const disabled = (0, utils_exports.isFunction)(enableRowSelection) && !enableRowSelection(row);
return /* @__PURE__ */ jsx(Tr, {
id: row.id,
"aria-disabled": (0, utils_exports.ariaAttr)(disabled),
"aria-rowindex": rowIndex + 1,
"aria-selected": (0, utils_exports.ariaAttr)(selected),
"data-disabled": (0, utils_exports.dataAttr)(disabled),
"data-selected": (0, utils_exports.dataAttr)(selected),
role: "row",
...mergeProps({ onClick: !disabled && selectOnClickRow ? () => row.toggleSelected(!selected) : void 0 }, {
onClick: !disabled ? () => onRowClick?.(row) : void 0,
onDoubleClick: !disabled ? () => onRowDoubleClick?.(row) : void 0
}, (0, utils_exports.runIfFn)(rowProps, row) ?? {})(),
children: row.getVisibleCells().map((cell) => {
const { columnDef } = cell.column;
const colIndex = cell.column.getIndex();
const tabIndex = getTabIndex(colIndex, rowIndex);
const children = flexRender(cell.column.columnDef.cell, cell.getContext());
return /* @__PURE__ */ jsx(Td, {
"aria-colindex": colIndex + 1,
"aria-rowindex": rowIndex + 1,
"data-colindex": colIndex,
"data-rowindex": rowIndex,
numeric: columnDef.numeric,
role: "gridcell",
tabIndex,
...mergeProps({ css: { "&:has([data-focusable]:focus-visible)": focusRingStyle.outline } }, (0, utils_exports.runIfFn)(cellProps, cell) ?? {}, columnDef.cellProps ?? {})(),
children: /* @__PURE__ */ jsx(TruncatedText, {
lineClamp: columnDef.lineClamp ?? lineClamp,
truncated: columnDef.truncated ?? truncated,
children
})
}, cell.id);
})
}, row.id);
})
}),
withFooterGroups ? /* @__PURE__ */ jsx(Tfoot, {
role: "rowgroup",
...tfootProps,
children: mergedFooterGroups.map((footerGroup, rowIndex) => {
rowIndex += headerGroupCount + rowCount;
let placeholderCount = 0;
return /* @__PURE__ */ jsx(Tr, {
"aria-rowindex": rowIndex + 1,
role: "row",
...(0, utils_exports.runIfFn)(footerGroupProps, footerGroup),
children: footerGroup.headers.map((header$1) => {
const { columnDef } = header$1.column;
const colIndex = header$1.index + placeholderCount;
const tabIndex = getTabIndex(colIndex, rowIndex);
const children = header$1.isPlaceholder ? null : flexRender(header$1.column.columnDef.footer, header$1.getContext());
placeholderCount += (header$1.colSpan || 1) - 1;
return /* @__PURE__ */ jsx(Td, {
"aria-colindex": colIndex + 1,
"aria-rowindex": rowIndex + 1,
"data-colindex": colIndex,
"data-rowindex": rowIndex,
colSpan: header$1.colSpan || void 0,
numeric: columnDef.numeric,
role: "gridcell",
rowSpan: header$1.rowSpan || void 0,
tabIndex,
...mergeProps({ css: { "&:has([data-focusable]:focus-visible)": focusRingStyle.outline } }, (0, utils_exports.runIfFn)(footerProps, header$1) ?? {}, columnDef.footerProps ?? {})(),
children: /* @__PURE__ */ jsx(TruncatedText, {
lineClamp: columnDef.lineClamp ?? lineClamp,
truncated: columnDef.truncated ?? truncated,
children
})
}, header$1.id);
})
}, footerGroup.id);
})
}) : null
]
}),
footer ? (0, utils_exports.runIfFn)(footer, table) : null
]
});
})();
const SortingIcon = ({ sorted,...rest }) => {
const { t } = useI18n("table");
const { sortingIcon, sortingIconProps = {} } = useComponentContext();
const Icon = sorted ? ChevronUpIcon : ChevronsUpDownIcon;
return /* @__PURE__ */ jsx(styled.button, {
type: "button",
layerStyle: "ghost",
colorScheme: "mono",
"aria-label": t(sorted ? sorted === "desc" ? "Sort descending" : "Sort ascending" : "Clear sorting"),
"data-focusable": true,
aspectRatio: "1",
cursor: "pointer",
display: "center",
focusVisibleRing: "none",
h: "calc(1em * {lineHeights.moderate})",
position: "absolute",
right: "{space-x}",
rounded: "l1",
tabIndex: -1,
top: "50%",
transform: "translateY(-50%)",
transitionDuration: "moderate",
transitionProperty: "common",
_hover: { layerStyle: "ghost.hover" },
...mergeProps(rest, sortingIconProps)(),
children: (0, utils_exports.runIfFn)(sortingIcon, sorted) ?? /* @__PURE__ */ jsx(Icon, { transform: `rotate(${sorted === "desc" ? 180 : 0}deg)` })
});
};
const ResizableTrigger = ({ resizing,...rest }) => {
const { columnResizeMode, table, resizableTriggerProps = {} } = useComponentContext();
const offset = table.getState().columnSizingInfo.deltaOffset;
return /* @__PURE__ */ jsx(styled.div, {
"data-active": (0, utils_exports.dataAttr)(resizing),
bg: "colorScheme.solid",
cursor: "col-resize",
insetY: "0",
opacity: {
base: "0",
_hover: "1",
_active: "1"
},
position: "absolute",
right: "0",
touchAction: "none",
transform: `translateX(${columnResizeMode === "onEnd" && resizing && offset ? `${offset}px` : "50%"})`,
userSelect: "none",
w: "1",
...mergeProps(rest, resizableTriggerProps)()
});
};
const TruncatedText = ({ children, lineClamp, truncated,...rest }) => {
if (lineClamp || truncated) return /* @__PURE__ */ jsx(styled.span, {
lineClamp,
truncated,
wordBreak: "break-all",
...rest,
children
});
else return children;
};
//#endregion
export { Table, TablePropsContext, createColumnHelper, useTablePropsContext };
//# sourceMappingURL=table.js.map