UNPKG

@hypothesis/frontend-shared

Version:

Shared components, styles and utilities for Hypothesis projects

329 lines (324 loc) 12.4 kB
var _jsxFileName = "/home/runner/work/frontend-shared/frontend-shared/src/components/data/DataTable.tsx"; import classnames from 'classnames'; import { useCallback, useContext, useEffect, useMemo } from 'preact/hooks'; import { useArrowKeyNavigation } from '../../hooks/use-arrow-key-navigation'; import { useStableCallback } from '../../hooks/use-stable-callback'; import { useSyncedRef } from '../../hooks/use-synced-ref'; import { downcastRef } from '../../util/typing'; import { ArrowDownIcon, ArrowUpIcon, OrderableIcon, SpinnerSpokesIcon } from '../icons'; import { Button } from '../input'; import ScrollContext from './ScrollContext'; import Table from './Table'; import TableBody from './TableBody'; import TableCell from './TableCell'; import TableFoot from './TableFoot'; import TableHead from './TableHead'; import TableRow from './TableRow'; import { jsxDEV as _jsxDEV, Fragment as _Fragment } from "preact/jsx-dev-runtime"; function defaultRenderItem(r, field) { return r[field]; } function calculateNewOrder(newField, prevOrder, initialOrderForColumn) { if (newField !== (prevOrder === null || prevOrder === void 0 ? void 0 : prevOrder.field)) { var _initialOrderForColum; return { field: newField, direction: (_initialOrderForColum = initialOrderForColumn === null || initialOrderForColumn === void 0 ? void 0 : initialOrderForColumn[newField]) !== null && _initialOrderForColum !== void 0 ? _initialOrderForColum : 'ascending' }; } const newDirection = prevOrder.direction === 'ascending' ? 'descending' : 'ascending'; return { field: newField, direction: newDirection }; } function HeaderComponent({ children, onClick, field }) { const commonClasses = 'flex justify-between items-center'; return onClick ? _jsxDEV(Button, { classes: `${commonClasses} w-full h-full !p-3`, variant: "custom", onClick: onClick, "data-testid": `${field}-order-button`, children: children }, void 0, false, { fileName: _jsxFileName, lineNumber: 135, columnNumber: 5 }, this) : _jsxDEV("div", { className: commonClasses, children: children }, void 0, false, { fileName: _jsxFileName, lineNumber: 144, columnNumber: 5 }, this); } /** * An interactive table of rows and columns with a sticky header. */ export default function DataTable({ children, elementRef, columns = [], rows = [], selectedRow, selectedRows, loading = false, renderItem = defaultRenderItem, onSelectRow, onSelectRows, onConfirmRow, emptyMessage, order, onOrderChange, orderableColumns = [], // Forwarded to Table title, borderless, striped, grid, ...htmlAttributes }) { const tableRef = useSyncedRef(elementRef); const scrollContext = useContext(ScrollContext); const [orderableColumnsList, initialOrderForColumn] = useMemo(() => Array.isArray(orderableColumns) ? [orderableColumns, {}] : [Object.keys(orderableColumns), orderableColumns], [orderableColumns]); const updateOrder = useCallback(newField => { const newOrder = calculateNewOrder(newField, order, initialOrderForColumn); onOrderChange === null || onOrderChange === void 0 || onOrderChange(newOrder); }, [initialOrderForColumn, onOrderChange, order]); const noContent = loading || !rows.length && emptyMessage; const fields = useMemo(() => columns.map(column => column.field), [columns]); const selectRow = useStableCallback((row, mode = 'replace') => { onSelectRow === null || onSelectRow === void 0 || onSelectRow(row); // If multi-selection is enabled, and the user shift+clicked the new row, // extend the selection from the "anchor" row (first entry in `selectedRows`) // to the just-clicked row. let newSelection = [row]; if (mode === 'extend' && selectedRows && selectedRows.length > 0) { const startIdx = rows.indexOf(selectedRows[0]); const endIdx = rows.indexOf(row); if (endIdx >= startIdx) { newSelection = rows.slice(startIdx, endIdx + 1); } else { // We reverse the selection here so that `startIdx` remains the first // entry in the list, and is used as the 'anchor' row for future // selections. newSelection = rows.slice(endIdx, startIdx + 1).reverse(); } } onSelectRows === null || onSelectRows === void 0 || onSelectRows(newSelection); }); useArrowKeyNavigation(tableRef, { selector: 'tbody tr', horizontal: true, vertical: true, focusElement: (element, keyEvent) => { // Simulate a click to update the selected row when arrow-key navigation // happens. We do this instead of using an `onFocus` handler on the row // itself because we need to know if the shift key was pressed, and // `FocusEvent` doesn't provide that information. if (keyEvent) { element.dispatchEvent(new MouseEvent('click', { // Propagate shift key state so arrow key + shift can be used to // create a multi-selection. shiftKey: keyEvent.shiftKey })); } // Scroll selected row into view. element.focus(); } }); const confirmRow = useStableCallback(row => { onConfirmRow === null || onConfirmRow === void 0 || onConfirmRow(row); }); const handleKeyDown = useCallback((event, row) => { // Avoid preventing Enter key interactions in children elements by // ignoring events not triggered by the row element itself if (event.key === 'Enter' && event.target === event.currentTarget) { confirmRow(row); event.preventDefault(); event.stopPropagation(); } }, [confirmRow]); // Ensure that a selected row is visible when this table is within // a scrolling context useEffect(() => { var _tableRef$current, _tableRef$current2; if (!selectedRow || !scrollContext) { return; } const scrollEl = scrollContext.scrollRef.current; const tableHead = (_tableRef$current = tableRef.current) === null || _tableRef$current === void 0 ? void 0 : _tableRef$current.querySelector('thead'); const selectedRowEl = (_tableRef$current2 = tableRef.current) === null || _tableRef$current2 === void 0 ? void 0 : _tableRef$current2.querySelector('tr[aria-selected="true"]'); if (scrollEl && selectedRowEl) { // Ensure the row is visible within the scroll content area const scrollOffset = selectedRowEl.offsetTop - scrollEl.scrollTop; if (scrollOffset > scrollEl.clientHeight) { selectedRowEl.scrollIntoView(); } // Ensure the row is not obscured by a sticky header if (tableHead) { const headingHeight = tableHead.clientHeight; const headingOffset = scrollOffset - headingHeight; if (headingOffset < 0) { scrollEl.scrollBy(0, headingOffset); } } } }, [selectedRow, tableRef, scrollContext]); // Render a <tfoot> element when there are any row data. This absorbs any // excess vertical space in tables with sparse rows data. const withFoot = !loading && rows.length > 0; const selection = useMemo(() => { if (selectedRows) { return selectedRows; } else if (selectedRow) { return [selectedRow]; } else { return []; } }, [selectedRows, selectedRow]); const tableRows = useMemo(() => { return rows.map((row, idx) => _jsxDEV(TableRow, { selected: selection.includes(row), onClick: e => selectRow(row, e.shiftKey ? 'extend' : 'replace'), onDblClick: () => confirmRow(row), onKeyDown: event => handleKeyDown(event, row), children: fields.map(field => _jsxDEV(TableCell, { children: renderItem(row, field) }, field, false, { fileName: _jsxFileName, lineNumber: 319, columnNumber: 11 }, this)) }, idx, false, { fileName: _jsxFileName, lineNumber: 311, columnNumber: 7 }, this)); }, [confirmRow, fields, renderItem, handleKeyDown, rows, selectRow, selection]); const interactive = Boolean(onSelectRow || onSelectRows || onConfirmRow); return _jsxDEV(Table, { "data-composite-component": "DataTable", role: "grid", ...htmlAttributes, elementRef: downcastRef(tableRef), interactive: interactive, stickyHeader: true, title: title, borderless: borderless, striped: striped, grid: grid, children: [_jsxDEV(TableHead, { children: _jsxDEV(TableRow, { children: columns.map(column => { const isOrderable = !!onOrderChange && orderableColumnsList.includes(column.field); const isActiveOrder = (order === null || order === void 0 ? void 0 : order.field) === column.field; return _jsxDEV(TableCell, { classes: column.classes, unpadded: isOrderable, "aria-sort": isActiveOrder ? order.direction : undefined, children: _jsxDEV(HeaderComponent, { field: column.field.toString(), onClick: isOrderable ? () => updateOrder(column.field) : undefined, children: [_jsxDEV("div", { children: column.label }, void 0, false, { fileName: _jsxFileName, lineNumber: 368, columnNumber: 19 }, this), isOrderable && _jsxDEV("div", { className: classnames('rounded p-1', { 'bg-white': isActiveOrder }), "aria-hidden": true, children: [isActiveOrder && (order.direction === 'ascending' ? _jsxDEV(ArrowUpIcon, {}, void 0, false, { fileName: _jsxFileName, lineNumber: 378, columnNumber: 27 }, this) : _jsxDEV(ArrowDownIcon, {}, void 0, false, { fileName: _jsxFileName, lineNumber: 380, columnNumber: 27 }, this)), !isActiveOrder && _jsxDEV(OrderableIcon, { className: classnames('text-grey-5', { // Interactive rows set a darker background color on // hover. // Setting a darker color on the icon when hovering // the row will ensure enough contrast. 'group-hover:text-grey-7': interactive }) }, void 0, false, { fileName: _jsxFileName, lineNumber: 383, columnNumber: 25 }, this)] }, void 0, true, { fileName: _jsxFileName, lineNumber: 370, columnNumber: 21 }, this)] }, void 0, true, { fileName: _jsxFileName, lineNumber: 362, columnNumber: 17 }, this) }, column.field, false, { fileName: _jsxFileName, lineNumber: 356, columnNumber: 15 }, this); }) }, void 0, false, { fileName: _jsxFileName, lineNumber: 349, columnNumber: 9 }, this) }, void 0, false, { fileName: _jsxFileName, lineNumber: 348, columnNumber: 7 }, this), _jsxDEV(TableBody, { children: [!loading && tableRows, noContent && _jsxDEV(TableRow, { children: _jsxDEV(TableCell, { colSpan: columns.length, classes: "text-center p-3", children: loading ? _jsxDEV(SpinnerSpokesIcon, { className: "inline w-2em h-2em" }, void 0, false, { fileName: _jsxFileName, lineNumber: 407, columnNumber: 17 }, this) : _jsxDEV(_Fragment, { children: emptyMessage }, void 0, false) }, void 0, false, { fileName: _jsxFileName, lineNumber: 405, columnNumber: 13 }, this) }, void 0, false, { fileName: _jsxFileName, lineNumber: 404, columnNumber: 11 }, this)] }, void 0, true, { fileName: _jsxFileName, lineNumber: 401, columnNumber: 7 }, this), children, withFoot && _jsxDEV(TableFoot, {}, void 0, false, { fileName: _jsxFileName, lineNumber: 416, columnNumber: 20 }, this)] }, void 0, true, { fileName: _jsxFileName, lineNumber: 336, columnNumber: 5 }, this); } //# sourceMappingURL=DataTable.js.map