@primer/react
Version:
An implementation of GitHub's Primer Design System using React
257 lines (243 loc) • 9.1 kB
JavaScript
import { useState } from 'react';
import { SortDirection, strategies, DEFAULT_SORT_DIRECTION, transition } from './sorting.js';
function useTable({
columns,
data,
initialSortColumn,
initialSortDirection
}) {
const [rowOrder, setRowOrder] = useState(data);
const [prevData, setPrevData] = useState(data);
const [prevColumns, setPrevColumns] = useState(columns);
const [sortByColumn, setSortByColumn] = useState(() => {
return getInitialSortState(columns, initialSortColumn, initialSortDirection);
});
const {
gridTemplateColumns
} = useTableLayout(columns);
// Reset the `sortByColumn` state if the columns change and that column is no
// longer provided
if (columns !== prevColumns) {
setPrevColumns(columns);
if (sortByColumn) {
const column = columns.find(column => {
var _column$id;
const id = (_column$id = column.id) !== null && _column$id !== void 0 ? _column$id : column.field;
return sortByColumn.id === id;
});
if (!column) {
setSortByColumn(null);
}
}
}
// Update the row order and apply the current sort column to the incoming data
if (data !== prevData) {
setPrevData(data);
setRowOrder(data);
if (sortByColumn) {
sortRows(sortByColumn);
}
}
const headers = columns.map(column => {
var _column$id2;
const id = (_column$id2 = column.id) !== null && _column$id2 !== void 0 ? _column$id2 : column.field;
if (id === undefined) {
throw new Error(`Expected either an \`id\` or \`field\` to be defined for a Column`);
}
const sortable = column.sortBy !== undefined && column.sortBy !== false;
return {
id,
column,
isSortable() {
return sortable;
},
getSortDirection() {
if (sortByColumn && sortByColumn.id === id) {
return sortByColumn.direction;
}
return SortDirection.NONE;
}
};
});
/**
* Sort the input row data by the given header
*/
function sortBy(header) {
const sortState = {
id: header.id,
direction: sortByColumn && sortByColumn.id === header.id ? transition(sortByColumn.direction) : DEFAULT_SORT_DIRECTION
};
setSortByColumn(sortState);
sortRows(sortState);
}
/**
* Sort the rows of a table with the given column sort state
*/
function sortRows(state) {
const header = headers.find(header => {
return header.id === state.id;
});
if (!header) {
throw new Error(`Unable to find header with id: ${state.id}`);
}
if (header.column.sortBy === false || header.column.sortBy === undefined) {
throw new Error(`The column for this header is not sortable`);
}
const sortMethod = header.column.sortBy === true ? strategies.basic : typeof header.column.sortBy === 'string' ? strategies[header.column.sortBy] : header.column.sortBy;
setRowOrder(rowOrder => {
return rowOrder.slice().sort((a, b) => {
if (header.column.field === undefined) {
return 0;
}
// Custom sort functions operate on the row versus the field
if (typeof header.column.sortBy === 'function') {
if (state.direction === SortDirection.ASC) {
// @ts-ignore todo
return sortMethod(a, b);
}
// @ts-ignore todo
return sortMethod(b, a);
}
const valueA = get(a, header.column.field);
const valueB = get(b, header.column.field);
if (state.direction === SortDirection.ASC) {
// @ts-ignore todo
return sortMethod(valueA, valueB);
}
// @ts-ignore todo
return sortMethod(valueB, valueA);
});
});
}
return {
headers,
rows: rowOrder.map(row => {
return {
id: `${row.id}`,
getValue() {
return row;
},
getCells() {
return headers.map(header => {
var _header$column$rowHea;
return {
id: `${row.id}:${header.id}`,
column: header.column,
rowHeader: (_header$column$rowHea = header.column.rowHeader) !== null && _header$column$rowHea !== void 0 ? _header$column$rowHea : false,
getValue() {
if (header.column.field !== undefined) {
return get(row, header.column.field);
}
throw new Error(`Unable to get value for column header ${header.id}`);
}
};
});
}
};
}),
actions: {
sortBy
},
gridTemplateColumns
};
}
function getInitialSortState(columns, initialSortColumn, initialSortDirection) {
if (initialSortColumn !== undefined) {
const column = columns.find(column => {
return column.id === initialSortColumn || column.field === initialSortColumn;
});
if (column === undefined) {
if (process.env.NODE_ENV !== "production") {
// eslint-disable-next-line no-console
console.warn(`Warning: Unable to find a column with id or field set to: ${initialSortColumn}. Please provide a value to \`initialSortColumn\` which corresponds to a \`id\` or \`field\` value in a column.`);
}
return null;
}
if (column.sortBy === false || column.sortBy === undefined) {
if (process.env.NODE_ENV !== "production") {
// eslint-disable-next-line no-console
console.warn(`Warning: The column specified by initialSortColumn={${initialSortColumn}} is not sortable. Please set \`sortBy\` to true or provide a sort strategy.`);
}
return null;
}
return {
id: `${initialSortColumn}`,
direction: initialSortDirection !== null && initialSortDirection !== void 0 ? initialSortDirection : DEFAULT_SORT_DIRECTION
};
}
if (initialSortDirection !== undefined) {
var _column$id3;
const column = columns.find(column => {
return column.sortBy !== false && column.sortBy !== undefined;
});
if (!column) {
if (process.env.NODE_ENV !== "production") {
// eslint-disable-next-line no-console
console.warn(`Warning: An initialSortDirection value was provided but no columns are sortable. Please set \`sortBy\` to true or provide a sort strategy to a column.`);
}
return null;
}
const id = (_column$id3 = column.id) !== null && _column$id3 !== void 0 ? _column$id3 : column.field;
if (id === undefined) {
if (process.env.NODE_ENV !== "production") {
// eslint-disable-next-line no-console
console.warn(`Warning: Unable to find an \`id\` or \`field\` for the column: ${column}. Please set one of these properties on the column.`);
}
return null;
}
return {
id,
direction: initialSortDirection
};
}
return null;
}
function useTableLayout(columns) {
return {
gridTemplateColumns: getGridTemplateFromColumns(columns).join(' ')
};
}
function getGridTemplateFromColumns(columns) {
return columns.map(column => {
var _column$width;
const columnWidth = (_column$width = column.width) !== null && _column$width !== void 0 ? _column$width : 'grow';
let minWidth = 'auto';
let maxWidth = '1fr';
if (columnWidth === 'auto') {
maxWidth = 'auto';
}
// Setting a min-width of 'max-content' ensures that the column will grow to fit the widest cell's content.
// However, If the column has a max width, we can't set the min width to `max-content` because
// the widest cell's content might overflow the container.
if (columnWidth === 'grow' && !column.maxWidth) {
minWidth = 'max-content';
}
// Column widths set to "growCollapse" don't need a min width unless one is explicitly provided.
if (columnWidth === 'growCollapse') {
minWidth = '0';
}
// If a consumer passes `minWidth` or `maxWidth`, we need to override whatever we set above.
if (column.minWidth) {
minWidth = typeof column.minWidth === 'number' ? `${column.minWidth}px` : column.minWidth;
}
if (column.maxWidth) {
maxWidth = typeof column.maxWidth === 'number' ? `${column.maxWidth}px` : column.maxWidth;
}
// If a consumer is passing one of the shorthand widths or doesn't pass a width at all, we use the
// min and max width calculated above to create a minmax() column template value.
if (typeof columnWidth !== 'number' && ['grow', 'growCollapse', 'auto'].includes(columnWidth)) {
return minWidth === maxWidth ? minWidth : `minmax(${minWidth}, ${maxWidth})`;
}
// If we reach this point, the consumer is passing an explicit width value.
return typeof columnWidth === 'number' ? `${columnWidth}px` : columnWidth;
});
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
function get(object, path) {
return path.split('.').reduce((value, key) => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
return value[key];
// eslint-disable-next-line @typescript-eslint/no-explicit-any
}, object);
}
export { getGridTemplateFromColumns, useTable, useTableLayout };