From 00e60f1acb0931b6918039c007ef1ac396740a8d Mon Sep 17 00:00:00 2001 From: WK Wong Date: Sun, 27 Apr 2025 21:12:15 +0800 Subject: [PATCH 01/10] feat(shared-icons): add SortIcon --- packages/utilities/shared-icons/src/index.ts | 1 + packages/utilities/shared-icons/src/sort.tsx | 37 ++++++++++++++++++++ 2 files changed, 38 insertions(+) create mode 100644 packages/utilities/shared-icons/src/sort.tsx diff --git a/packages/utilities/shared-icons/src/index.ts b/packages/utilities/shared-icons/src/index.ts index 1be0e2b205..c48773147d 100644 --- a/packages/utilities/shared-icons/src/index.ts +++ b/packages/utilities/shared-icons/src/index.ts @@ -13,6 +13,7 @@ export * from "./ellipsis"; export * from "./forward"; export * from "./sun"; export * from "./sun-filled"; +export * from "./sort"; export * from "./mail"; export * from "./mail-filled"; export * from "./moon"; diff --git a/packages/utilities/shared-icons/src/sort.tsx b/packages/utilities/shared-icons/src/sort.tsx new file mode 100644 index 0000000000..e71e6a0e2d --- /dev/null +++ b/packages/utilities/shared-icons/src/sort.tsx @@ -0,0 +1,37 @@ +import {IconSvgProps} from "./types"; + +export const SortIcon = (props: IconSvgProps) => ( + + + + + + +); From 6e7a18d5a6799d5f588c33d43430ee725b3fd51f Mon Sep 17 00:00:00 2001 From: WK Wong Date: Sun, 27 Apr 2025 21:12:42 +0800 Subject: [PATCH 02/10] feat(table): add CustomSortIcon story --- packages/components/table/stories/table.stories.tsx | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/packages/components/table/stories/table.stories.tsx b/packages/components/table/stories/table.stories.tsx index e34fa8cd52..1ce20870b0 100644 --- a/packages/components/table/stories/table.stories.tsx +++ b/packages/components/table/stories/table.stories.tsx @@ -7,7 +7,7 @@ import {Button} from "@heroui/button"; import {Spinner} from "@heroui/spinner"; import {Pagination} from "@heroui/pagination"; import {Tooltip} from "@heroui/tooltip"; -import {EditIcon, DeleteIcon, EyeIcon} from "@heroui/shared-icons"; +import {EditIcon, DeleteIcon, EyeIcon, SortIcon} from "@heroui/shared-icons"; import {useInfiniteScroll} from "@heroui/use-infinite-scroll"; import {useAsyncList} from "@react-stately/data"; import useSWR from "swr"; @@ -1065,6 +1065,15 @@ export const Sortable = { }, }; +export const CustomSortIcon = { + render: SortableTemplate, + + args: { + ...defaultProps, + sortIcon: SortIcon, + }, +}; + export const LoadMore = { render: LoadMoreTemplate, From f70e93c0fda91877b6ddbecfc06a25b030c92573 Mon Sep 17 00:00:00 2001 From: WK Wong Date: Sun, 27 Apr 2025 21:21:11 +0800 Subject: [PATCH 03/10] feat(table): support custom sort icon --- .../table/src/table-column-header.tsx | 30 ++++++++++++------- packages/components/table/src/table.tsx | 2 ++ packages/components/table/src/use-table.ts | 6 ++++ 3 files changed, 28 insertions(+), 10 deletions(-) diff --git a/packages/components/table/src/table-column-header.tsx b/packages/components/table/src/table-column-header.tsx index 4ed281f9a6..8c924f04ec 100644 --- a/packages/components/table/src/table-column-header.tsx +++ b/packages/components/table/src/table-column-header.tsx @@ -1,5 +1,7 @@ import type {GridNode} from "@react-types/grid"; +import type {ReactNode, ReactElement} from "react"; +import {cloneElement, isValidElement} from "react"; import {forwardRef, HTMLHeroUIProps} from "@heroui/system"; import {useDOMRef, filterDOMProps} from "@heroui/react-utils"; import {clsx, dataAttr} from "@heroui/shared-utils"; @@ -17,6 +19,10 @@ export interface TableColumnHeaderProps extends HTMLHeroUIProps<"th" slots: ValuesType["slots"]; state: ValuesType["state"]; classNames?: ValuesType["classNames"]; + /** + * Custom Icon to be displayed in the table header - overrides the default chevron one + */ + sortIcon?: ReactNode | ((props: any) => ReactNode); /** * The table node to render. */ @@ -24,7 +30,7 @@ export interface TableColumnHeaderProps extends HTMLHeroUIProps<"th" } const TableColumnHeader = forwardRef<"th", TableColumnHeaderProps>((props, ref) => { - const {as, className, state, node, slots, classNames, ...otherProps} = props; + const {as, className, state, node, slots, classNames, sortIcon, ...otherProps} = props; const Component = as || "th"; const shouldFilterDOMProps = typeof Component === "string"; @@ -41,6 +47,18 @@ const TableColumnHeader = forwardRef<"th", TableColumnHeaderProps>((props, ref) const allowsSorting = columnProps.allowsSorting; + const sortIconProps = { + "aria-hidden": true, + "data-direction": state.sortDescriptor?.direction, + "data-visible": dataAttr(state.sortDescriptor?.column === node.key), + className: slots.sortIcon?.({class: classNames?.sortIcon}), + }; + + const customSortIcon = + sortIcon && isValidElement(sortIcon) + ? cloneElement(sortIcon as ReactElement, sortIconProps) + : null; + return ( ((props, ref) className={slots.th?.({align, class: thStyles})} > {hideHeader ? {node.rendered} : node.rendered} - {allowsSorting && ( - ); }); diff --git a/packages/components/table/src/table.tsx b/packages/components/table/src/table.tsx index 317f5e5a81..9d0e817fc2 100644 --- a/packages/components/table/src/table.tsx +++ b/packages/components/table/src/table.tsx @@ -28,6 +28,7 @@ const Table = forwardRef<"table", TableProps>((props, ref) => { bottomContentPlacement, bottomContent, removeWrapper, + sortIcon, getBaseProps, getWrapperProps, getTableProps, @@ -101,6 +102,7 @@ const Table = forwardRef<"table", TableProps>((props, ref) => { classNames={values.classNames} node={column} slots={values.slots} + sortIcon={sortIcon} state={values.state} /> ), diff --git a/packages/components/table/src/use-table.ts b/packages/components/table/src/use-table.ts index a3eea8d320..d0996fbe44 100644 --- a/packages/components/table/src/use-table.ts +++ b/packages/components/table/src/use-table.ts @@ -89,6 +89,10 @@ interface Props extends HTMLHeroUIProps<"table"> { * Props to be passed to the checkboxes. */ checkboxesProps?: CheckboxProps; + /** + * Custom Icon to be displayed in the table header - overrides the default chevron one + */ + sortIcon?: ReactNode | ((props: any) => ReactNode); /** Handler that is called when a user performs an action on the row. */ onRowAction?: (key: Key) => void; /** Handler that is called when a user performs an action on the cell. */ @@ -164,6 +168,7 @@ export function useTable(originalProps: UseTableProps) { checkboxesProps, topContent, bottomContent, + sortIcon, onRowAction, onCellAction, ...otherProps @@ -292,6 +297,7 @@ export function useTable(originalProps: UseTableProps) { removeWrapper, topContentPlacement, bottomContentPlacement, + sortIcon, getBaseProps, getWrapperProps, getTableProps, From 4577504f87953a29d617e3a8b53cc9b93188d25b Mon Sep 17 00:00:00 2001 From: WK Wong Date: Sun, 27 Apr 2025 21:30:23 +0800 Subject: [PATCH 04/10] fix(table): handle functional sortIcon --- packages/components/table/src/table-column-header.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/components/table/src/table-column-header.tsx b/packages/components/table/src/table-column-header.tsx index 8c924f04ec..6c43e6ac4c 100644 --- a/packages/components/table/src/table-column-header.tsx +++ b/packages/components/table/src/table-column-header.tsx @@ -55,9 +55,9 @@ const TableColumnHeader = forwardRef<"th", TableColumnHeaderProps>((props, ref) }; const customSortIcon = - sortIcon && isValidElement(sortIcon) - ? cloneElement(sortIcon as ReactElement, sortIconProps) - : null; + typeof sortIcon === "function" + ? sortIcon(sortIconProps) + : isValidElement(sortIcon) && cloneElement(sortIcon as ReactElement, sortIconProps); return ( Date: Sun, 27 Apr 2025 21:34:52 +0800 Subject: [PATCH 05/10] chore(changeset): add changeset --- .changeset/heavy-otters-knock.md | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 .changeset/heavy-otters-knock.md diff --git a/.changeset/heavy-otters-knock.md b/.changeset/heavy-otters-knock.md new file mode 100644 index 0000000000..3d4bb0f277 --- /dev/null +++ b/.changeset/heavy-otters-knock.md @@ -0,0 +1,6 @@ +--- +"@heroui/shared-icons": patch +"@heroui/table": patch +--- + +support custom sort icon in Table (#5223) From b2206d0fe2312a9d3e875ab0fffc9f63d5aac9c8 Mon Sep 17 00:00:00 2001 From: WK Wong Date: Fri, 2 May 2025 19:36:48 +0800 Subject: [PATCH 06/10] chore(table): update type --- packages/components/table/src/table-column-header.tsx | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/packages/components/table/src/table-column-header.tsx b/packages/components/table/src/table-column-header.tsx index 6c43e6ac4c..2c044d16ee 100644 --- a/packages/components/table/src/table-column-header.tsx +++ b/packages/components/table/src/table-column-header.tsx @@ -15,6 +15,13 @@ import {useHover} from "@react-aria/interactions"; import {ValuesType} from "./use-table"; // @internal +export type SortIconProps = { + "aria-hidden"?: boolean; + "data-direction"?: "ascending" | "descending"; + "data-visible"?: boolean | "true" | "false"; + className?: string; +}; + export interface TableColumnHeaderProps extends HTMLHeroUIProps<"th"> { slots: ValuesType["slots"]; state: ValuesType["state"]; @@ -22,7 +29,7 @@ export interface TableColumnHeaderProps extends HTMLHeroUIProps<"th" /** * Custom Icon to be displayed in the table header - overrides the default chevron one */ - sortIcon?: ReactNode | ((props: any) => ReactNode); + sortIcon?: ReactNode | ((props: SortIconProps) => ReactNode); /** * The table node to render. */ From 7e1a3bc3d064817b90f96f9d819bc9bd7bf20cb5 Mon Sep 17 00:00:00 2001 From: WK Wong Date: Fri, 2 May 2025 19:37:02 +0800 Subject: [PATCH 07/10] feat(docs): add sortIcon to table --- apps/docs/content/docs/components/table.mdx | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/apps/docs/content/docs/components/table.mdx b/apps/docs/content/docs/components/table.mdx index 141eadcca5..f870e25390 100644 --- a/apps/docs/content/docs/components/table.mdx +++ b/apps/docs/content/docs/components/table.mdx @@ -735,6 +735,11 @@ You can customize the `Table` component by passing custom Tailwind CSS classes t type: "boolean", description: "Whether the column allows sorting", default: "-" + }, + attribute: "sortIcon", + type: "ReactNode", + description: "Overrides the default sort icon. Only applied when `allowsSorting` is `true`", + default: "-" }, { attribute: "isRowHeader", From 7093d6439ba82671bbf58d9fb92e7c304bfe9315 Mon Sep 17 00:00:00 2001 From: WK Wong Date: Fri, 2 May 2025 19:51:49 +0800 Subject: [PATCH 08/10] fix(docs): broken object --- apps/docs/content/docs/components/table.mdx | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/docs/content/docs/components/table.mdx b/apps/docs/content/docs/components/table.mdx index f870e25390..1d38c4d3a3 100644 --- a/apps/docs/content/docs/components/table.mdx +++ b/apps/docs/content/docs/components/table.mdx @@ -736,6 +736,7 @@ You can customize the `Table` component by passing custom Tailwind CSS classes t description: "Whether the column allows sorting", default: "-" }, + { attribute: "sortIcon", type: "ReactNode", description: "Overrides the default sort icon. Only applied when `allowsSorting` is `true`", From b7d7ee7ca669124ec46bfce5695624b5cd50daff Mon Sep 17 00:00:00 2001 From: WK Wong Date: Sun, 15 Jun 2025 12:21:42 +0800 Subject: [PATCH 09/10] chore(shared-icons): lint --- packages/utilities/shared-icons/src/sort.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/utilities/shared-icons/src/sort.tsx b/packages/utilities/shared-icons/src/sort.tsx index e71e6a0e2d..0a2a4505ab 100644 --- a/packages/utilities/shared-icons/src/sort.tsx +++ b/packages/utilities/shared-icons/src/sort.tsx @@ -1,4 +1,4 @@ -import {IconSvgProps} from "./types"; +import type {IconSvgProps} from "./types"; export const SortIcon = (props: IconSvgProps) => ( Date: Thu, 3 Jul 2025 22:19:47 +0800 Subject: [PATCH 10/10] feat(docs): add example for sort icon --- .../app/examples/table/sort-icon/page.tsx | 96 +++++++++++++++++++ apps/docs/content/components/table/index.ts | 2 + .../components/table/sort-icon.raw.jsx | 84 ++++++++++++++++ .../content/components/table/sort-icon.ts | 9 ++ apps/docs/content/docs/components/table.mdx | 14 +++ 5 files changed, 205 insertions(+) create mode 100644 apps/docs/app/examples/table/sort-icon/page.tsx create mode 100644 apps/docs/content/components/table/sort-icon.raw.jsx create mode 100644 apps/docs/content/components/table/sort-icon.ts diff --git a/apps/docs/app/examples/table/sort-icon/page.tsx b/apps/docs/app/examples/table/sort-icon/page.tsx new file mode 100644 index 0000000000..b410570505 --- /dev/null +++ b/apps/docs/app/examples/table/sort-icon/page.tsx @@ -0,0 +1,96 @@ +"use client"; + +import { + Table, + TableHeader, + TableColumn, + TableBody, + TableRow, + TableCell, + getKeyValue, + Spinner, +} from "@heroui/react"; +import {SortIcon} from "@heroui/shared-icons"; +import {useAsyncList} from "@react-stately/data"; +import {useState} from "react"; + +type SWCharacter = { + name: string; + height: string; + mass: string; + birth_year: string; +}; + +export default function Page() { + const [isLoading, setIsLoading] = useState(true); + + let list = useAsyncList({ + async load({signal}) { + let res = await fetch(`https://swapi.py4e.com/api/people/?search`, { + signal, + }); + let json = await res.json(); + + setIsLoading(false); + + return { + items: json.results, + }; + }, + async sort({items, sortDescriptor}) { + return { + items: items.sort((a, b) => { + let first = a[sortDescriptor.column as keyof SWCharacter]; + let second = b[sortDescriptor.column as keyof SWCharacter]; + let cmp = (parseInt(first) || first) < (parseInt(second) || second) ? -1 : 1; + + if (sortDescriptor.direction === "descending") { + cmp *= -1; + } + + return cmp; + }), + }; + }, + }); + + return ( +
+ + + + Name + + + Height + + + Mass + + + Birth year + + + } + > + {(item) => ( + + {(columnKey) => {getKeyValue(item, columnKey)}} + + )} + +
+
+ ); +} diff --git a/apps/docs/content/components/table/index.ts b/apps/docs/content/components/table/index.ts index 19a8fbe483..4b2462c288 100644 --- a/apps/docs/content/components/table/index.ts +++ b/apps/docs/content/components/table/index.ts @@ -13,6 +13,7 @@ import disabledRows from "./disabled-rows"; import selectionBehavior from "./selection-behavior"; import rowActions from "./row-actions"; import sorting from "./sorting"; +import sortIcon from "./sort-icon"; import loadMore from "./load-more"; import paginated from "./paginated"; import asyncPagination from "./async-pagination"; @@ -40,6 +41,7 @@ export const tableContent = { selectionBehavior, rowActions, sorting, + sortIcon, loadMore, paginated, asyncPagination, diff --git a/apps/docs/content/components/table/sort-icon.raw.jsx b/apps/docs/content/components/table/sort-icon.raw.jsx new file mode 100644 index 0000000000..e934c9a60c --- /dev/null +++ b/apps/docs/content/components/table/sort-icon.raw.jsx @@ -0,0 +1,84 @@ +import { + Table, + TableHeader, + TableColumn, + TableBody, + TableRow, + TableCell, + getKeyValue, + Spinner, +} from "@heroui/react"; +import {SortIcon} from "@heroui/shared-icons"; +import {useAsyncList} from "@react-stately/data"; + +export default function App() { + const [isLoading, setIsLoading] = React.useState(true); + + let list = useAsyncList({ + async load({signal}) { + let res = await fetch("https://swapi.py4e.com/api/people/?search", { + signal, + }); + let json = await res.json(); + + setIsLoading(false); + + return { + items: json.results, + }; + }, + async sort({items, sortDescriptor}) { + return { + items: items.sort((a, b) => { + let first = a[sortDescriptor.column]; + let second = b[sortDescriptor.column]; + let cmp = (parseInt(first) || first) < (parseInt(second) || second) ? -1 : 1; + + if (sortDescriptor.direction === "descending") { + cmp *= -1; + } + + return cmp; + }), + }; + }, + }); + + return ( + + + + Name + + + Height + + + Mass + + + Birth year + + + } + > + {(item) => ( + + {(columnKey) => {getKeyValue(item, columnKey)}} + + )} + +
+ ); +} diff --git a/apps/docs/content/components/table/sort-icon.ts b/apps/docs/content/components/table/sort-icon.ts new file mode 100644 index 0000000000..60305fa04b --- /dev/null +++ b/apps/docs/content/components/table/sort-icon.ts @@ -0,0 +1,9 @@ +import App from "./sort-icon.raw.jsx?raw"; + +const react = { + "/App.jsx": App, +}; + +export default { + ...react, +}; diff --git a/apps/docs/content/docs/components/table.mdx b/apps/docs/content/docs/components/table.mdx index 7eded00906..797e2ab87a 100644 --- a/apps/docs/content/docs/components/table.mdx +++ b/apps/docs/content/docs/components/table.mdx @@ -247,6 +247,20 @@ import {useAsyncList} from "@react-stately/data"; > Note that we passed the `isLoading` and `loadingContent` props to `TableBody` to > render a loading state while the data is being fetched. +### Sort Icon + +You can override the default sort icon by specifying `sortIcon`. This prop is only applied when `allowsSorting` is `true`. + + + ### Loading more data Table allows you to add a custom component at the end of the table, on the example below we are