diff --git a/cypress/component/DataViewTableSorting.cy.tsx b/cypress/component/DataViewTableSorting.cy.tsx
new file mode 100644
index 0000000..79cae0f
--- /dev/null
+++ b/cypress/component/DataViewTableSorting.cy.tsx
@@ -0,0 +1,109 @@
+/* eslint-disable no-nested-ternary */
+import React from 'react';
+import { useDataViewSort } from '@patternfly/react-data-view/dist/dynamic/Hooks';
+import { DataViewTable, DataViewTr, DataViewTh } from '@patternfly/react-data-view/dist/dynamic/DataViewTable';
+import { BrowserRouter, useSearchParams } from 'react-router-dom';
+import { ThProps } from '@patternfly/react-table';
+
+interface Repository {
+ name: string;
+ branches: string;
+ prs: string;
+ workspaces: string;
+ lastCommit: string;
+}
+
+const COLUMNS = [
+ { label: 'Repository', key: 'name', index: 0 },
+ { label: 'Branch', key: 'branches', index: 1 },
+ { label: 'Pull request', key: 'prs', index: 2 },
+ { label: 'Workspace', key: 'workspaces', index: 3 },
+ { label: 'Last commit', key: 'lastCommit', index: 4 },
+];
+
+const repositories: Repository[] = [
+ { name: 'Repository one', branches: 'Branch one', prs: 'Pull request one', workspaces: 'Workspace one', lastCommit: '2023-11-01' },
+ { name: 'Repository six', branches: 'Branch six', prs: 'Pull request six', workspaces: 'Workspace six', lastCommit: '2023-11-06' },
+ { name: 'Repository two', branches: 'Branch two', prs: 'Pull request two', workspaces: 'Workspace two', lastCommit: '2023-11-02' },
+ { name: 'Repository five', branches: 'Branch five', prs: 'Pull request five', workspaces: 'Workspace five', lastCommit: '2023-11-05' },
+ { name: 'Repository three', branches: 'Branch three', prs: 'Pull request three', workspaces: 'Workspace three', lastCommit: '2023-11-03' },
+ { name: 'Repository four', branches: 'Branch four', prs: 'Pull request four', workspaces: 'Workspace four', lastCommit: '2023-11-04' },
+];
+
+const sortData = (data: Repository[], sortBy: keyof Repository | undefined, direction: 'asc' | 'desc' | undefined) =>
+ sortBy && direction
+ ? [ ...data ].sort((a, b) =>
+ direction === 'asc'
+ ? a[sortBy] < b[sortBy] ? -1 : a[sortBy] > b[sortBy] ? 1 : 0
+ : a[sortBy] > b[sortBy] ? -1 : a[sortBy] < b[sortBy] ? 1 : 0
+ )
+ : data;
+
+const TestTable: React.FunctionComponent = () => {
+ const [ searchParams, setSearchParams ] = useSearchParams();
+ const { sortBy, direction, onSort } = useDataViewSort({ searchParams, setSearchParams });
+ const sortByIndex = React.useMemo(() => COLUMNS.findIndex(item => item.key === sortBy), [ sortBy ]);
+
+ const getSortParams = (columnIndex: number): ThProps['sort'] => ({
+ sortBy: {
+ index: sortByIndex,
+ direction,
+ defaultDirection: 'asc',
+ },
+ onSort: (_event, index, direction) => onSort(_event, COLUMNS[index].key, direction),
+ columnIndex,
+ });
+
+ const columns: DataViewTh[] = COLUMNS.map((column, index) => ({
+ cell: column.label,
+ props: { sort: getSortParams(index) },
+ }));
+
+ const rows: DataViewTr[] = React.useMemo(
+ () =>
+ sortData(repositories, sortBy ? sortBy as keyof Repository : undefined, direction).map(({ name, branches, prs, workspaces, lastCommit }) => [
+ name,
+ branches,
+ prs,
+ workspaces,
+ lastCommit,
+ ]),
+ [ sortBy, direction ]
+ );
+
+ return ;
+};
+
+describe('DataViewTable Sorting with Hook', () => {
+ it('sorts by repository name in ascending and descending order', () => {
+ cy.mount(
+
+
+
+ );
+
+ cy.get('[data-ouia-component-id="test-table-th-0"]').click();
+ cy.get('[data-ouia-component-id="test-table-td-0-0"]').should('contain', 'Repository five');
+ cy.get('[data-ouia-component-id="test-table-td-5-0"]').should('contain', 'Repository two');
+
+ cy.get('[data-ouia-component-id="test-table-th-0"]').click();
+ cy.get('[data-ouia-component-id="test-table-td-0-0"]').should('contain', 'Repository two');
+ cy.get('[data-ouia-component-id="test-table-td-5-0"]').should('contain', 'Repository five');
+ });
+
+ it('sorts by last commit date in ascending and descending order', () => {
+ cy.mount(
+
+
+
+ );
+
+ cy.get('[data-ouia-component-id="test-table-th-4"]').click();
+ cy.get('[data-ouia-component-id="test-table-td-0-4"]').should('contain', '2023-11-01');
+ cy.get('[data-ouia-component-id="test-table-td-5-4"]').should('contain', '2023-11-06');
+
+ cy.get('[data-ouia-component-id="test-table-th-4"]').click();
+ cy.get('[data-ouia-component-id="test-table-td-0-4"]').should('contain', '2023-11-06');
+ cy.get('[data-ouia-component-id="test-table-td-5-4"]').should('contain', '2023-11-01');
+ });
+});
diff --git a/packages/module/patternfly-docs/content/extensions/data-view/examples/Functionality/Functionality.md b/packages/module/patternfly-docs/content/extensions/data-view/examples/Functionality/Functionality.md
index 1e31d79..7469001 100644
--- a/packages/module/patternfly-docs/content/extensions/data-view/examples/Functionality/Functionality.md
+++ b/packages/module/patternfly-docs/content/extensions/data-view/examples/Functionality/Functionality.md
@@ -16,7 +16,7 @@ sourceLink: https://github.com/patternfly/react-data-view/blob/main/packages/mod
---
import { useMemo } from 'react';
import { BrowserRouter, useSearchParams } from 'react-router-dom';
-import { useDataViewPagination, useDataViewSelection, useDataViewFilters } from '@patternfly/react-data-view/dist/dynamic/Hooks';
+import { useDataViewPagination, useDataViewSelection, useDataViewFilters, useDataViewSort } from '@patternfly/react-data-view/dist/dynamic/Hooks';
import { DataView } from '@patternfly/react-data-view/dist/dynamic/DataView';
import { BulkSelect, BulkSelectValue } from '@patternfly/react-component-groups/dist/dynamic/BulkSelect';
import { DataViewToolbar } from '@patternfly/react-data-view/dist/dynamic/DataViewToolbar';
@@ -119,3 +119,34 @@ This example demonstrates the setup and usage of filters within the data view. I
```js file="./FiltersExample.tsx"
```
+
+### Sort state
+
+The `useDataViewSort` hook manages the sorting state of a data view. It provides an easy way to handle sorting logic, including synchronization with URL parameters and defining default sorting behavior.
+
+**Initial values:**
+- `initialSort` object to set default `sortBy` and `direction` values:
+ - `sortBy`: key of the initial column to sort.
+ - `direction`: default sorting direction (`asc` or `desc`).
+- Optional `searchParams` object to manage URL-based synchronization of sort state.
+- Optional `setSearchParams` function to update the URL parameters when sorting changes.
+- `defaultDirection` to set the default direction when no direction is specified.
+- Customizable parameter names for the URL:
+ - `sortByParam`: name of the URL parameter for the column key.
+ - `directionParam`: name of the URL parameter for the sorting direction.
+
+The `useDataViewSort` hook integrates seamlessly with React Router to manage sort state via URL parameters. Alternatively, you can use `URLSearchParams` and `window.history.pushState` APIs, or other routing libraries. If URL synchronization is not configured, the sort state is managed internally within the component.
+
+**Return values:**
+- `sortBy`: key of the column currently being sorted.
+- `direction`: current sorting direction (`asc` or `desc`).
+- `onSort`: function to handle sorting changes programmatically or via user interaction.
+
+### Sorting example
+
+This example demonstrates how to set up and use sorting functionality within a data view. The implementation includes dynamic sorting by column with persistence of sort state in the URL using React Router.
+
+
+```js file="./SortingExample.tsx"
+
+```
diff --git a/packages/module/patternfly-docs/content/extensions/data-view/examples/Functionality/SortingExample.tsx b/packages/module/patternfly-docs/content/extensions/data-view/examples/Functionality/SortingExample.tsx
new file mode 100644
index 0000000..879f1cd
--- /dev/null
+++ b/packages/module/patternfly-docs/content/extensions/data-view/examples/Functionality/SortingExample.tsx
@@ -0,0 +1,87 @@
+/* eslint-disable no-nested-ternary */
+import React, { useMemo } from 'react';
+import { useDataViewSort } from '@patternfly/react-data-view/dist/dynamic/Hooks';
+import { DataViewTable, DataViewTr, DataViewTh } from '@patternfly/react-data-view/dist/dynamic/DataViewTable';
+import { ThProps } from '@patternfly/react-table';
+import { BrowserRouter, useSearchParams } from 'react-router-dom';
+
+interface Repository {
+ name: string;
+ branches: string;
+ prs: string;
+ workspaces: string;
+ lastCommit: string;
+};
+
+const COLUMNS = [
+ { label: 'Repository', key: 'name', index: 0 },
+ { label: 'Branch', key: 'branches', index: 1 },
+ { label: 'Pull request', key: 'prs', index: 2 },
+ { label: 'Workspace', key: 'workspaces', index: 3 },
+ { label: 'Last commit', key: 'lastCommit', index: 4 }
+];
+
+const repositories: Repository[] = [
+ { name: 'Repository one', branches: 'Branch one', prs: 'Pull request one', workspaces: 'Workspace one', lastCommit: 'Timestamp one' },
+ { name: 'Repository two', branches: 'Branch two', prs: 'Pull request two', workspaces: 'Workspace two', lastCommit: 'Timestamp two' },
+ { name: 'Repository three', branches: 'Branch three', prs: 'Pull request three', workspaces: 'Workspace three', lastCommit: 'Timestamp three' },
+ { name: 'Repository four', branches: 'Branch four', prs: 'Pull request four', workspaces: 'Workspace four', lastCommit: 'Timestamp four' },
+ { name: 'Repository five', branches: 'Branch five', prs: 'Pull request five', workspaces: 'Workspace five', lastCommit: 'Timestamp five' },
+ { name: 'Repository six', branches: 'Branch six', prs: 'Pull request six', workspaces: 'Workspace six', lastCommit: 'Timestamp six' }
+];
+
+const sortData = (data: Repository[], sortBy: string | undefined, direction: 'asc' | 'desc' | undefined) =>
+ sortBy && direction
+ ? [ ...data ].sort((a, b) =>
+ direction === 'asc'
+ ? a[sortBy] < b[sortBy] ? -1 : a[sortBy] > b[sortBy] ? 1 : 0
+ : a[sortBy] > b[sortBy] ? -1 : a[sortBy] < b[sortBy] ? 1 : 0
+ )
+ : data;
+
+const ouiaId = 'TableExample';
+
+export const MyTable: React.FunctionComponent = () => {
+ const [ searchParams, setSearchParams ] = useSearchParams();
+ const { sortBy, direction, onSort } = useDataViewSort({ searchParams, setSearchParams });
+ const sortByIndex = useMemo(() => COLUMNS.findIndex(item => item.key === sortBy), [ sortBy ]);
+
+ const getSortParams = (columnIndex: number): ThProps['sort'] => ({
+ sortBy: {
+ index: sortByIndex,
+ direction,
+ defaultDirection: 'asc'
+ },
+ onSort: (_event, index, direction) => onSort(_event, COLUMNS[index].key, direction),
+ columnIndex
+ });
+
+ const columns: DataViewTh[] = COLUMNS.map((column, index) => ({
+ cell: column.label,
+ props: { sort: getSortParams(index) }
+ }));
+
+ const rows: DataViewTr[] = useMemo(() => sortData(repositories, sortBy, direction).map(({ name, branches, prs, workspaces, lastCommit }) => [
+ name,
+ branches,
+ prs,
+ workspaces,
+ lastCommit,
+ ]), [ sortBy, direction ]);
+
+ return (
+
+ );
+};
+
+export const BasicExample: React.FunctionComponent = () => (
+
+
+
+)
+
diff --git a/packages/module/src/Hooks/index.ts b/packages/module/src/Hooks/index.ts
index 546a0da..3ce609c 100644
--- a/packages/module/src/Hooks/index.ts
+++ b/packages/module/src/Hooks/index.ts
@@ -1,3 +1,4 @@
export * from './pagination';
export * from './selection';
export * from './filters';
+export * from './sort';
diff --git a/packages/module/src/Hooks/sort.test.tsx b/packages/module/src/Hooks/sort.test.tsx
new file mode 100644
index 0000000..473924d
--- /dev/null
+++ b/packages/module/src/Hooks/sort.test.tsx
@@ -0,0 +1,84 @@
+import '@testing-library/jest-dom';
+import { renderHook, act } from '@testing-library/react';
+import { useDataViewSort, UseDataViewSortProps, DataViewSortConfig, DataViewSortParams } from './sort';
+
+describe('useDataViewSort', () => {
+ const initialSort: DataViewSortConfig = { sortBy: 'name', direction: 'asc' };
+
+ it('should initialize with provided initial sort config', () => {
+ const { result } = renderHook(() => useDataViewSort({ initialSort }));
+ expect(result.current).toEqual(expect.objectContaining(initialSort));
+ });
+
+ it('should initialize with empty sort config if no initialSort is provided', () => {
+ const { result } = renderHook(() => useDataViewSort());
+ expect(result.current).toEqual(expect.objectContaining({ sortBy: undefined, direction: 'asc' }));
+ });
+
+ it('should update sort state when onSort is called', () => {
+ const { result } = renderHook(() => useDataViewSort({ initialSort }));
+ act(() => {
+ result.current.onSort(undefined, 'age', 'desc');
+ });
+ expect(result.current).toEqual(expect.objectContaining({ sortBy: 'age', direction: 'desc' }));
+ });
+
+ it('should sync with URL search params if isUrlSyncEnabled', () => {
+ const searchParams = new URLSearchParams();
+ const setSearchParams = jest.fn();
+ const props: UseDataViewSortProps = {
+ initialSort,
+ searchParams,
+ setSearchParams,
+ };
+
+ const { result } = renderHook(() => useDataViewSort(props));
+
+ expect(setSearchParams).toHaveBeenCalledTimes(1);
+ expect(result.current).toEqual(expect.objectContaining(initialSort));
+ });
+
+ it('should validate direction and fallback to default direction if invalid direction is provided', () => {
+ const searchParams = new URLSearchParams();
+ searchParams.set(DataViewSortParams.SORT_BY, 'name');
+ searchParams.set(DataViewSortParams.DIRECTION, 'invalid-direction');
+ const { result } = renderHook(() => useDataViewSort({ searchParams, defaultDirection: 'desc' }));
+
+ expect(result.current).toEqual(expect.objectContaining({ sortBy: 'name', direction: 'desc' }));
+ });
+
+ it('should update search params when URL sync is enabled and sort changes', () => {
+ const searchParams = new URLSearchParams();
+ const setSearchParams = jest.fn();
+ const props: UseDataViewSortProps = {
+ initialSort,
+ searchParams,
+ setSearchParams,
+ };
+
+ const { result } = renderHook(() => useDataViewSort(props));
+ act(() => {
+ expect(setSearchParams).toHaveBeenCalledTimes(1);
+ result.current.onSort(undefined, 'priority', 'desc');
+ });
+
+ expect(setSearchParams).toHaveBeenCalledTimes(2);
+ expect(result.current).toEqual(expect.objectContaining({ sortBy: 'priority', direction: 'desc' }));
+ });
+
+ it('should prioritize searchParams values', () => {
+ const searchParams = new URLSearchParams();
+ searchParams.set(DataViewSortParams.SORT_BY, 'category');
+ searchParams.set(DataViewSortParams.DIRECTION, 'desc');
+
+ const { result } = renderHook(
+ (props: UseDataViewSortProps) => useDataViewSort(props),
+ { initialProps: { initialSort, searchParams } }
+ );
+
+ expect(result.current).toEqual(expect.objectContaining({
+ sortBy: 'category',
+ direction: 'desc',
+ }));
+ });
+});
diff --git a/packages/module/src/Hooks/sort.ts b/packages/module/src/Hooks/sort.ts
new file mode 100644
index 0000000..ed06f22
--- /dev/null
+++ b/packages/module/src/Hooks/sort.ts
@@ -0,0 +1,87 @@
+import { ISortBy } from "@patternfly/react-table";
+import { useState, useEffect, useMemo } from "react";
+
+export enum DataViewSortParams {
+ SORT_BY = 'sortBy',
+ DIRECTION = 'direction'
+};
+
+const validateDirection = (direction: string | null | undefined, defaultDirection: ISortBy['direction']): ISortBy['direction'] => (
+ direction === 'asc' || direction === 'desc' ? direction : defaultDirection
+);
+
+export interface DataViewSortConfig {
+ /** Attribute to sort the entries by */
+ sortBy: string | undefined;
+ /** Sort direction */
+ direction: ISortBy['direction'];
+};
+
+export interface UseDataViewSortProps {
+ /** Initial sort config */
+ initialSort?: DataViewSortConfig;
+ /** Current search parameters as a string */
+ searchParams?: URLSearchParams;
+ /** Function to set search parameters */
+ setSearchParams?: (params: URLSearchParams) => void;
+ /** Default direction */
+ defaultDirection?: ISortBy['direction'];
+ /** Sort by URL param name */
+ sortByParam?: string;
+ /** Direction URL param name */
+ directionParam?: string;
+};
+
+export const useDataViewSort = (props?: UseDataViewSortProps) => {
+ const {
+ initialSort,
+ searchParams,
+ setSearchParams,
+ defaultDirection = 'asc',
+ sortByParam = DataViewSortParams.SORT_BY,
+ directionParam = DataViewSortParams.DIRECTION
+ } = props ?? {};
+
+ const isUrlSyncEnabled = useMemo(() => searchParams && !!setSearchParams, [ searchParams, setSearchParams ]);
+
+ const [ state, setState ] = useState({
+ sortBy: searchParams?.get(sortByParam) ?? initialSort?.sortBy,
+ direction: validateDirection(searchParams?.get(directionParam) as ISortBy['direction'], initialSort?.direction),
+ });
+
+ const updateSearchParams = (sortBy: string, direction: ISortBy['direction']) => {
+ if (isUrlSyncEnabled && sortBy) {
+ const params = new URLSearchParams(searchParams);
+ params.set(sortByParam, `${sortBy}`);
+ params.set(directionParam, `${direction}`);
+ setSearchParams?.(params);
+ }
+ };
+
+ useEffect(() => {
+ state.sortBy && state.direction && updateSearchParams(state.sortBy, state.direction);
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, []);
+
+ useEffect(() => {
+ const currentSortBy = searchParams?.get(sortByParam) || state.sortBy;
+ const currentDirection = searchParams?.get(directionParam) as ISortBy['direction'] || state.direction;
+ const validDirection = validateDirection(currentDirection, defaultDirection);
+ currentSortBy !== state.sortBy || validDirection !== state.direction && setState({ sortBy: currentSortBy, direction: validDirection });
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [ searchParams?.toString() ]);
+
+ const onSort = (
+ _event: React.MouseEvent | React.KeyboardEvent | MouseEvent | undefined,
+ newSortBy: string,
+ newSortDirection: ISortBy['direction']
+ ) => {
+ setState({ sortBy: newSortBy, direction: newSortDirection });
+ updateSearchParams(newSortBy, newSortDirection);
+ };
+
+ return {
+ ...state,
+ onSort
+ };
+};