UNPKG

@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
"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