From c8a38d412fc218f8f8995f11a6f310e43cfdf859 Mon Sep 17 00:00:00 2001 From: Felix Beceic Date: Tue, 4 Jun 2024 15:42:47 +0200 Subject: [PATCH] #236 DataTable - add `retainDefaultSortBy` prop for preserving default sorting --- libs/data-display/src/DataTable.tsx | 67 ++++++++++++++++--- .../data-display/src/useSortableDataTable.tsx | 11 ++- .../src/data-display/DataTable.stories.tsx | 62 +++++++++++++---- 3 files changed, 117 insertions(+), 23 deletions(-) diff --git a/libs/data-display/src/DataTable.tsx b/libs/data-display/src/DataTable.tsx index 945c394..98bddc5 100644 --- a/libs/data-display/src/DataTable.tsx +++ b/libs/data-display/src/DataTable.tsx @@ -17,9 +17,15 @@ import * as React from "react"; -import { every, find, flatMap, identity, isEqual, pickBy, sum } from "lodash"; -import _ from "lodash"; -import { Cell, Column, HeaderProps, Row, useExpanded, useRowSelect, useSortBy, useTable } from "react-table"; +import { every, find, flatMap, slice, identity, isEqual, pickBy, sum, xorWith } from "lodash"; +import { + Cell, + Column, + HeaderProps, + Row, + SortingRule, + useExpanded, + useRowSelect, useSortBy, useTable } from "react-table"; import { Card, CardHeaderProps } from "@tiller-ds/core"; import { Checkbox } from "@tiller-ds/form-elements"; @@ -55,6 +61,19 @@ export type DataTableProps = { */ defaultSortBy?: SortInfo[]; + /** + * Controls table sorting behavior when clicking a column header by returning to a default sorted state defined by `defaultSortBy` prop when sort resets. + * + * By default, clicking a column header cycles through sorting states, and a sort reset returns the table to an unsorted state. + * Setting this prop to `true` prevents this default behavior. + * + * With this prop enabled, clicking the header of a column defined in `defaultSortBy` will toggle between ascending and descending order **even after a sort reset**. + * This allows you to maintain the initial sort order throughout user interaction. + * + * @defaultValue false + */ + retainDefaultSortBy?: boolean; + /** * For getting each item's unique identifier on DataTable initialization. * Ex. (item: Item) => item.id @@ -368,6 +387,8 @@ type UseDataTable = [ isAllRowsSelected: boolean; sortBy: SortInfo[]; + + defaultSortBy: SortInfo[]; }, DataTableHook, @@ -442,7 +463,7 @@ export function useDataTable({ defaultSortBy = [] }: UseDataTableProps = {}): Us ); const state = React.useMemo( - () => ({ selected, selectedCount, isAllRowsSelected, sortBy }), + () => ({ selected, selectedCount, isAllRowsSelected, sortBy, defaultSortBy }), [selected, selectedCount, isAllRowsSelected, sortBy], ); @@ -457,7 +478,7 @@ export function useDataTable({ defaultSortBy = [] }: UseDataTableProps = {}): Us [updateSelected, updateSortBy, selected, isAllRowsSelected, toggleSelectAll], ); - return [state, hook]; + return [state, hook] as UseDataTable; } type Operation = "sum" | "average"; @@ -505,8 +526,11 @@ function DataTable({ lastColumnFixed, className, multiSort = false, + retainDefaultSortBy, ...props }: DataTableProps) { + const tokens = useTokens("DataTable", props.tokens); + const primaryRow = findChild("DataTablePrimaryRow", children); const secondaryRow = findChild("DataTableSecondaryRow", children); const hasSecondaryColumns = React.isValidElement(secondaryRow); @@ -565,7 +589,19 @@ function DataTable({ }, [getItemId], ); - const tokens = useTokens("DataTable", props.tokens); + + const mapToSortingRules = (sortBy: SortInfo[], flipCols: string[]): SortingRule[] => { + return sortBy.map((sortInfo) => ({ + id: sortInfo.column, + desc: flipCols.includes(sortInfo.column) + ? sortInfo.sortDirection !== "DESCENDING" + : sortInfo.sortDirection === "DESCENDING", + })); + }; + + const differenceOfSortingRules = (rule1: SortingRule[], rule2: SortingRule[]): SortingRule[] => + xorWith(rule1, rule2, (r1, r2) => r1.id === r2.id && r1.desc === r2.desc); + const { getTableProps, getTableBodyProps, @@ -580,6 +616,21 @@ function DataTable({ { columns, data, + stateReducer: (newState, action, prevState) => { + const hasNoSort = newState.sortBy.length === 0; + const sortingDifference = differenceOfSortingRules(newState.sortBy, sortBy); + + if (retainDefaultSortBy && action.type === "toggleSortBy" && (sortingDifference.length === 0 || hasNoSort)) { + const sortedCols = sortingDifference.map((rule) => rule.id); + + return { + ...newState, + sortBy: isEqual(prevState.sortBy, sortBy) ? mapToSortingRules(defaultSortBy, sortedCols) : sortBy, + }; + } + + return newState; + }, autoResetPage: false, autoResetSelectedRows: false, autoResetSortBy: false, @@ -713,7 +764,7 @@ function DataTable({ rows.map((row, rowKey) => { prepareRow(row); - const primaryCells = _.slice(row.cells, 0, columnChildrenSize); + const primaryCells = slice(row.cells, 0, columnChildrenSize); const secondaryCells = hasSecondaryColumns ? row.cells.slice(columnChildrenSize, totalColumnChildrenSize) : []; @@ -1174,7 +1225,7 @@ const isConfigurationEqual = (prevChildren, nextChildren) => { } const getConfiguration = (children: Array>) => children.flatMap((child) => (React.isValidElement(child) ? child.props.type : undefined)); - return _.isEqual(getConfiguration(prevChildArray), getConfiguration(nextChildArray)); + return isEqual(getConfiguration(prevChildArray), getConfiguration(nextChildArray)); }; const MemoDataTable = React.memo( diff --git a/libs/data-display/src/useSortableDataTable.tsx b/libs/data-display/src/useSortableDataTable.tsx index 46d0d35..35d7fa5 100644 --- a/libs/data-display/src/useSortableDataTable.tsx +++ b/libs/data-display/src/useSortableDataTable.tsx @@ -1,9 +1,14 @@ import { useMemo } from "react"; import { useDataTable } from "./index"; +import { SortInfo } from "./DataTable"; -export default function useSortableDataTable(initialData: T[], columnMapping: Record) { - const [dataTableState, dataTableHook] = useDataTable(); +export default function useSortableDataTable( + initialData: T[], + columnMapping: Record, + defaultSortBy?: SortInfo[], +) { + const [dataTableState, dataTableHook] = useDataTable({ defaultSortBy }); const generateSortedData = useMemo(() => { const sortInstructions = dataTableState.sortBy; @@ -15,7 +20,7 @@ export default function useSortableDataTable(initialData: return [...initialData].sort((a, b) => { for (const sortInfo of sortInstructions) { const columnKey = columnMapping[sortInfo.column]; - const compareResult = sortInfo.sortDirection === "ASCENDING" ? -1 : 1; + const compareResult = sortInfo.sortDirection === "ASCENDING" ? 1 : -1; const aValue = a[columnKey]; const bValue = b[columnKey]; diff --git a/storybook/src/data-display/DataTable.stories.tsx b/storybook/src/data-display/DataTable.stories.tsx index 765ed96..d384581 100644 --- a/storybook/src/data-display/DataTable.stories.tsx +++ b/storybook/src/data-display/DataTable.stories.tsx @@ -23,7 +23,7 @@ import { range, slice } from "lodash"; import { withDesign } from "storybook-addon-designs"; import { Button, Card, IconButton, Link, Pagination, Typography, useLocalPagination } from "@tiller-ds/core"; -import { DataTable, useDataTable, useLocalSummary, useSortableDataTable } from "@tiller-ds/data-display"; +import { DataTable, SortInfo, useDataTable, useLocalSummary, useSortableDataTable } from "@tiller-ds/data-display"; import { Icon } from "@tiller-ds/icons"; import { DropdownMenu } from "@tiller-ds/menu"; import { defaultThemeConfig } from "@tiller-ds/theme"; @@ -1432,6 +1432,7 @@ export const WithDefaultAscendingSortByName = (args) => { + ); }; @@ -1443,26 +1444,63 @@ export const WithDefaultAscendingSortUsingHook = () => { surname: "surname", }; - const { dataTableHook, sortedData } = useSortableDataTable(allData || [], columnMapping); + const defaultSortBy: SortInfo[] = [ + { + column: "name", + sortDirection: "DESCENDING", + }, + { + column: "surname", + sortDirection: "DESCENDING", + }, + ]; + + const { dataTableHook, sortedData, dataTableState } = useSortableDataTable( + allData || [], + columnMapping, + defaultSortBy, + ); + + return ( + + + + + + ); +}; + +export const WithDefaultAscendingSortWithRetainedInitialSort = () => { + // incl-code + const columnMapping = { + name: "name", + surname: "surname", + }; + + const defaultSortBy: SortInfo[] = [ + { + column: "name", + sortDirection: "DESCENDING", + }, + ]; + + const { dataTableHook, sortedData, dataTableState } = useSortableDataTable( + allData || [], + columnMapping, + defaultSortBy, + ); return ( + ); };