From 8be027b5ecf75b0ef491b1392540aad5764ea59f Mon Sep 17 00:00:00 2001 From: benjaminLeongSK Date: Mon, 13 Apr 2026 07:50:07 +0800 Subject: [PATCH 01/16] [#1073][BL] Add aria label for header checkbox --- src/data-table/data-table.tsx | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/data-table/data-table.tsx b/src/data-table/data-table.tsx index 58aa07e51e..efa57b3804 100644 --- a/src/data-table/data-table.tsx +++ b/src/data-table/data-table.tsx @@ -150,6 +150,11 @@ export const DataTable = ({ return headers.length + (enableMultiSelect ? 1 : 0); }; + const getHeaderCheckboxAriaLabel = (): string => { + const totalColumns = getTotalColumns(); + return `Select all rows, Column 1 of ${totalColumns}`; + }; + const calculateFixedInViewport = () => { if (!wrapperRef.current) { return; @@ -277,12 +282,14 @@ export const DataTable = ({ data-testid={getDataTestId("header-selection")} $clickable={false} $isCheckbox={true} + scope="col" > {enableSelectAll && ( { if (onSelectAll) { onSelectAll(isAllCheckboxSelected()); From 059dae1ad3f1bf78a95c813d2cb2b72709365452 Mon Sep 17 00:00:00 2001 From: benjaminLeongSK Date: Mon, 13 Apr 2026 07:51:53 +0800 Subject: [PATCH 02/16] [#1073][BL] Introduce keyFieldLabel props for checkbox aria label --- src/data-table/data-table.tsx | 72 +++++++++++++++++++++++++++++++-- src/data-table/types.ts | 2 + stories/data-table/row-data.tsx | 2 + 3 files changed, 72 insertions(+), 4 deletions(-) diff --git a/src/data-table/data-table.tsx b/src/data-table/data-table.tsx index efa57b3804..00796fb6be 100644 --- a/src/data-table/data-table.tsx +++ b/src/data-table/data-table.tsx @@ -1,5 +1,12 @@ import { ArrowDownIcon, ArrowUpIcon } from "@lifesg/react-icons"; -import { useCallback, useEffect, useRef, useState } from "react"; +import { + ReactNode, + isValidElement, + useCallback, + useEffect, + useRef, + useState, +} from "react"; import { useInView } from "react-intersection-observer"; import { useResizeDetector } from "react-resize-detector"; import { LoadingDotsSpinner } from "../animations"; @@ -155,6 +162,61 @@ export const DataTable = ({ return `Select all rows, Column 1 of ${totalColumns}`; }; + const extractTextFromNode = (node: ReactNode): string => { + if (node === null || node === undefined || typeof node === "boolean") { + return ""; + } + + if (typeof node === "string" || typeof node === "number") { + return node.toString(); + } + + if (Array.isArray(node)) { + return node.map(extractTextFromNode).join(" ").trim(); + } + + if (isValidElement(node)) { + return extractTextFromNode(node.props.children); + } + + return ""; + }; + + const getKeyFieldText = (header: HeaderProps, row: RowProps): string => { + if (typeof header === "string") { + return ""; + } + + if (!header.keyFieldLabel) { + return ""; + } + + const cellData = row[header.fieldKey]; + + if (typeof cellData === "function") { + return extractTextFromNode( + cellData(row, { + isSelected: isRowSelected(row.id.toString()), + }) + ); + } + + return extractTextFromNode(cellData); + }; + + const getRowCheckboxAriaLabel = (row: RowProps, index: number): string => { + const keyFieldText = headers + .map((header) => getKeyFieldText(header, row)) + .filter((value) => value.length > 0) + .join(". "); + + const rowPositionText = `Row ${index + 1} of ${rows?.length ?? 0}`; + + return keyFieldText + ? `${rowPositionText}. ${keyFieldText}` + : rowPositionText; + }; + const calculateFixedInViewport = () => { if (!wrapperRef.current) { return; @@ -321,8 +383,7 @@ export const DataTable = ({ $isSelectable={enableMultiSelect} $isSelected={isRowSelected(row.id.toString())} > - {enableMultiSelect && - renderRowCheckBox(row.id.toString())} + {enableMultiSelect && renderRowCheckBox(row, index)} {headers.map((header) => renderRowCell(header, row))} @@ -357,7 +418,9 @@ export const DataTable = ({ ); }; - const renderRowCheckBox = (rowId: string) => { + const renderRowCheckBox = (row: RowProps, index: number) => { + const rowId = row.id.toString(); + return ( { if (onSelect) { onSelect(rowId, !isRowSelected(rowId)); diff --git a/src/data-table/types.ts b/src/data-table/types.ts index bdfa03f101..b0bf60c78c 100644 --- a/src/data-table/types.ts +++ b/src/data-table/types.ts @@ -56,6 +56,8 @@ interface HeaderItemProps { // technically ReactNode also includes string, but this is more explicit label: string | ReactNode; clickable?: boolean | undefined; + /** Marks this column as part of the row checkbox screen reader label */ + keyFieldLabel?: boolean | undefined; style?: CSSProperties | undefined; } diff --git a/stories/data-table/row-data.tsx b/stories/data-table/row-data.tsx index df457376c9..53687958aa 100644 --- a/stories/data-table/row-data.tsx +++ b/stories/data-table/row-data.tsx @@ -28,10 +28,12 @@ export const DATA_HEADERS = [ { fieldKey: "status", label: "Status", + keyFieldLabel: true, }, { fieldKey: "resource", label: "Resource", + keyFieldLabel: true, }, { fieldKey: "time", From 459642f6ce5fa3af7b079fe7e4c935f99b5d78e0 Mon Sep 17 00:00:00 2001 From: benjaminLeongSK Date: Mon, 13 Apr 2026 08:54:36 +0800 Subject: [PATCH 03/16] [#1073][BL] Make clickable headers tabbable and introduce new sortable prop --- src/data-table/data-table.styles.tsx | 10 +++ src/data-table/data-table.tsx | 74 +++++++++++++++++++---- src/data-table/types.ts | 1 + stories/data-table/data-table.stories.tsx | 1 + 4 files changed, 75 insertions(+), 11 deletions(-) diff --git a/src/data-table/data-table.styles.tsx b/src/data-table/data-table.styles.tsx index ac91a9d190..af8d110617 100644 --- a/src/data-table/data-table.styles.tsx +++ b/src/data-table/data-table.styles.tsx @@ -211,6 +211,16 @@ export const HeaderCellWrapper = styled.div` } `; +export const ClickableHeader = styled(BasicButton)` + ${Font["body-md-semibold"]} + display: inline-flex; + align-items: center; + width: 100%; + color: ${fontColor}; + cursor: pointer; + text-align: left; +`; + export const BodyRow = styled.tr` background-color: ${(props) => { if (props.$isSelected) { diff --git a/src/data-table/data-table.tsx b/src/data-table/data-table.tsx index 00796fb6be..51cd211b46 100644 --- a/src/data-table/data-table.tsx +++ b/src/data-table/data-table.tsx @@ -21,6 +21,7 @@ import { BodyCellContent, BodyRow, CheckBoxWrapper, + ClickableHeader, EmptyViewCell, ErrorDisplayTitle, HeaderCell, @@ -162,6 +163,31 @@ export const DataTable = ({ return `Select all rows, Column 1 of ${totalColumns}`; }; + const getSortDirection = (fieldKey: string) => { + return sortIndicators?.[fieldKey]; + }; + + const getHeaderAriaSort = (fieldKey: string, sortable: boolean) => { + if (!sortable) { + return undefined; + } + + const sortDirection = getSortDirection(fieldKey); + + if (!sortDirection) { + return undefined; + } + + return sortDirection === "asc" ? "ascending" : "descending"; + }; + + const getSortButtonAriaLabel = (label: string, fieldKey: string) => { + const nextSortDirection = + getSortDirection(fieldKey) === "asc" ? "descending" : "ascending"; + + return `Sort ${label} by ${nextSortDirection} order`; + }; + const extractTextFromNode = (node: ReactNode): string => { if (node === null || node === undefined || typeof node === "boolean") { return ""; @@ -286,11 +312,13 @@ export const DataTable = ({ fieldKey, label, clickable = false, + sortable = false, style, } = typeof header === "string" ? { fieldKey: header, label: header, + sortable: false, style: undefined, } : header; @@ -300,20 +328,44 @@ export const DataTable = ({ data-testid={getDataTestId(`header-${fieldKey}`)} key={fieldKey} $clickable={clickable} - onClick={() => clickable && onHeaderClick?.(fieldKey)} + scope="col" + aria-sort={getHeaderAriaSort(fieldKey, sortable)} style={style} $isCheckbox={false} > - - {typeof label === "string" ? ( - - {label} - - ) : ( - label - )} - {renderSortedArrow(fieldKey)} - + {clickable ? ( + onHeaderClick?.(fieldKey)} + > + + {typeof label === "string" ? ( + + {label} + + ) : ( + label + )} + {renderSortedArrow(fieldKey)} + + + ) : ( + + {typeof label === "string" ? ( + + {label} + + ) : ( + label + )} + {renderSortedArrow(fieldKey)} + + )} ); }; diff --git a/src/data-table/types.ts b/src/data-table/types.ts index b0bf60c78c..5d7e64827f 100644 --- a/src/data-table/types.ts +++ b/src/data-table/types.ts @@ -56,6 +56,7 @@ interface HeaderItemProps { // technically ReactNode also includes string, but this is more explicit label: string | ReactNode; clickable?: boolean | undefined; + sortable?: boolean | undefined; /** Marks this column as part of the row checkbox screen reader label */ keyFieldLabel?: boolean | undefined; style?: CSSProperties | undefined; diff --git a/stories/data-table/data-table.stories.tsx b/stories/data-table/data-table.stories.tsx index f934737570..ed0fe05665 100644 --- a/stories/data-table/data-table.stories.tsx +++ b/stories/data-table/data-table.stories.tsx @@ -99,6 +99,7 @@ export const SortIndicators: StoryObj = { fieldKey: "colA", label: "Column A", clickable: true, + sortable: true, }, { fieldKey: "colB", From 7b894110a98d0cde2e4a5a0dd9af0d68721213b6 Mon Sep 17 00:00:00 2001 From: benjaminLeongSK Date: Mon, 13 Apr 2026 09:11:13 +0800 Subject: [PATCH 04/16] [#1073][BL] Add accessible label for other states --- src/data-table/data-table.tsx | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/src/data-table/data-table.tsx b/src/data-table/data-table.tsx index 51cd211b46..79a2371476 100644 --- a/src/data-table/data-table.tsx +++ b/src/data-table/data-table.tsx @@ -488,6 +488,7 @@ export const DataTable = ({ } }} disabled={isDisabledRow(rowId)} + aria-disabled={isDisabledRow(rowId)} /> @@ -527,7 +528,11 @@ export const DataTable = ({ return ( - + {loadState === "loading" && } @@ -554,7 +559,13 @@ export const DataTable = ({ {`${count} item${ count > 1 ? "s" : "" } selected`} - + Clear selection {actionBarContent} From b2575fae3bee1a06bb5f747cf4992676bd675cd4 Mon Sep 17 00:00:00 2001 From: benjaminLeongSK Date: Mon, 13 Apr 2026 09:35:22 +0800 Subject: [PATCH 05/16] [#1073][BL] Fix accessibility issues --- src/data-table/data-table.tsx | 2 ++ stories/data-table/doc-elements.tsx | 9 ++++++++- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/src/data-table/data-table.tsx b/src/data-table/data-table.tsx index 79a2371476..93be8f46b1 100644 --- a/src/data-table/data-table.tsx +++ b/src/data-table/data-table.tsx @@ -12,6 +12,7 @@ import { useResizeDetector } from "react-resize-detector"; import { LoadingDotsSpinner } from "../animations"; import { Checkbox } from "../checkbox"; import { ErrorDisplay } from "../error-display"; +import { VisuallyHidden } from "../shared/accessibility"; import { Typography } from "../typography"; import { useEventListener } from "../util/use-event-listener"; import { @@ -399,6 +400,7 @@ export const DataTable = ({ scope="col" > + Row selection {enableSelectAll && ( { const [height, setHeight] = useState("default"); + const rowCountInputId = "data-table-row-count"; + return (
Modify the options to view the scroll behaviour - + Number of rows
Date: Mon, 13 Apr 2026 10:17:44 +0800 Subject: [PATCH 06/16] [#1073][BL] Update props table --- stories/data-table/props-table.tsx | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/stories/data-table/props-table.tsx b/stories/data-table/props-table.tsx index 9962f3252e..6e08404105 100644 --- a/stories/data-table/props-table.tsx +++ b/stories/data-table/props-table.tsx @@ -157,12 +157,29 @@ const DATA: ApiTableSectionProps[] = [ ), propTypes: ["boolean"], }, + { + name: "sortable", + description: ( + <> + Specifies if the column header is sortable. When true,{" "} + onHeaderClick will be called when the cell + is clicked + + ), + propTypes: ["boolean"], + }, { name: "style", description: "Specifies custom styles for the column header cell", propTypes: ["CSSProperties"], }, + { + name: "keyFieldLabel", + description: + "Marks this column as part of the row checkbox screen reader label. Applicable when checkbox is presented for row selection", + propTypes: ["boolean"], + }, ], }, { From 5ee26863ec6329d11ff364766173e38016582531 Mon Sep 17 00:00:00 2001 From: benjaminLeongSK Date: Mon, 13 Apr 2026 11:36:21 +0800 Subject: [PATCH 07/16] [#1073][BL] Shift header label out of ClickableHeader --- src/data-table/data-table.styles.tsx | 18 ++++++++++++------ src/data-table/data-table.tsx | 23 +++++++++++++---------- 2 files changed, 25 insertions(+), 16 deletions(-) diff --git a/src/data-table/data-table.styles.tsx b/src/data-table/data-table.styles.tsx index af8d110617..d633e74d53 100644 --- a/src/data-table/data-table.styles.tsx +++ b/src/data-table/data-table.styles.tsx @@ -184,6 +184,7 @@ export const HeaderRow = styled.tr` `; export const HeaderCell = styled.th` + position: relative; padding: ${(props) => props.$isCheckbox ? "1.25rem 0.5rem 1.25rem 1.5rem" : "1.25rem 1rem"}; text-align: left; @@ -201,6 +202,8 @@ export const HeaderCell = styled.th` `; export const HeaderCellWrapper = styled.div` + position: relative; + z-index: 1; display: flex; flex-direction: row; align-items: center; @@ -212,13 +215,16 @@ export const HeaderCellWrapper = styled.div` `; export const ClickableHeader = styled(BasicButton)` - ${Font["body-md-semibold"]} - display: inline-flex; - align-items: center; - width: 100%; - color: ${fontColor}; + position: absolute; + inset: 0.25rem; + z-index: 2; cursor: pointer; - text-align: left; + border-radius: ${Radius["sm"]}; + + &:focus-visible { + outline: 2px solid ${Colour["focus-ring"]}; + outline-offset: 0; + } `; export const BodyRow = styled.tr` diff --git a/src/data-table/data-table.tsx b/src/data-table/data-table.tsx index 93be8f46b1..16c5deb440 100644 --- a/src/data-table/data-table.tsx +++ b/src/data-table/data-table.tsx @@ -335,15 +335,18 @@ export const DataTable = ({ $isCheckbox={false} > {clickable ? ( - onHeaderClick?.(fieldKey)} - > + <> + onHeaderClick?.(fieldKey)} + /> {typeof label === "string" ? ( @@ -354,7 +357,7 @@ export const DataTable = ({ )} {renderSortedArrow(fieldKey)} - + ) : ( {typeof label === "string" ? ( From 52c4d0223f855f9ebc4a73a645cf2cbc9051b9bc Mon Sep 17 00:00:00 2001 From: benjaminLeongSK Date: Tue, 14 Apr 2026 00:04:20 +0800 Subject: [PATCH 08/16] [#1073][BL] Remove sortable prop --- src/data-table/data-table.tsx | 14 +++++--------- src/data-table/types.ts | 1 - stories/data-table/data-table.stories.tsx | 1 - stories/data-table/props-table.tsx | 11 ----------- 4 files changed, 5 insertions(+), 22 deletions(-) diff --git a/src/data-table/data-table.tsx b/src/data-table/data-table.tsx index 16c5deb440..767716bb84 100644 --- a/src/data-table/data-table.tsx +++ b/src/data-table/data-table.tsx @@ -168,11 +168,7 @@ export const DataTable = ({ return sortIndicators?.[fieldKey]; }; - const getHeaderAriaSort = (fieldKey: string, sortable: boolean) => { - if (!sortable) { - return undefined; - } - + const getHeaderAriaSort = (fieldKey: string) => { const sortDirection = getSortDirection(fieldKey); if (!sortDirection) { @@ -313,24 +309,24 @@ export const DataTable = ({ fieldKey, label, clickable = false, - sortable = false, style, } = typeof header === "string" ? { fieldKey: header, label: header, - sortable: false, style: undefined, } : header; + const isSortable = !!sortIndicators?.[fieldKey]; + return ( @@ -339,7 +335,7 @@ export const DataTable = ({ = { fieldKey: "colA", label: "Column A", clickable: true, - sortable: true, }, { fieldKey: "colB", diff --git a/stories/data-table/props-table.tsx b/stories/data-table/props-table.tsx index 6e08404105..02cb29b275 100644 --- a/stories/data-table/props-table.tsx +++ b/stories/data-table/props-table.tsx @@ -157,17 +157,6 @@ const DATA: ApiTableSectionProps[] = [ ), propTypes: ["boolean"], }, - { - name: "sortable", - description: ( - <> - Specifies if the column header is sortable. When true,{" "} - onHeaderClick will be called when the cell - is clicked - - ), - propTypes: ["boolean"], - }, { name: "style", description: From 566e04ab7deda4eb3c08a86d51b75765627c138d Mon Sep 17 00:00:00 2001 From: benjaminLeongSK Date: Tue, 14 Apr 2026 00:06:18 +0800 Subject: [PATCH 09/16] [#1073][BL] Change prop name --- src/data-table/data-table.tsx | 2 +- src/data-table/types.ts | 7 +++++-- stories/data-table/props-table.tsx | 4 ++-- stories/data-table/row-data.tsx | 4 ++-- 4 files changed, 10 insertions(+), 7 deletions(-) diff --git a/src/data-table/data-table.tsx b/src/data-table/data-table.tsx index 767716bb84..46c0cbab1f 100644 --- a/src/data-table/data-table.tsx +++ b/src/data-table/data-table.tsx @@ -210,7 +210,7 @@ export const DataTable = ({ return ""; } - if (!header.keyFieldLabel) { + if (!header.keyColumn) { return ""; } diff --git a/src/data-table/types.ts b/src/data-table/types.ts index b0bf60c78c..0d47c31fd1 100644 --- a/src/data-table/types.ts +++ b/src/data-table/types.ts @@ -56,8 +56,11 @@ interface HeaderItemProps { // technically ReactNode also includes string, but this is more explicit label: string | ReactNode; clickable?: boolean | undefined; - /** Marks this column as part of the row checkbox screen reader label */ - keyFieldLabel?: boolean | undefined; + /** Used with `enableMultiSelect`. Marks this column as descriptive of the row + * (e.g. a name or unique ID). Its value is used to label the row's checkbox + * for screen readers. + */ + keyColumn?: boolean | undefined; style?: CSSProperties | undefined; } diff --git a/stories/data-table/props-table.tsx b/stories/data-table/props-table.tsx index 02cb29b275..b22401573f 100644 --- a/stories/data-table/props-table.tsx +++ b/stories/data-table/props-table.tsx @@ -164,9 +164,9 @@ const DATA: ApiTableSectionProps[] = [ propTypes: ["CSSProperties"], }, { - name: "keyFieldLabel", + name: "keyColumn", description: - "Marks this column as part of the row checkbox screen reader label. Applicable when checkbox is presented for row selection", + "Used with enableMultiSelect. Marks this column as descriptive of the row and uses its value to label the row checkbox for screen readers", propTypes: ["boolean"], }, ], diff --git a/stories/data-table/row-data.tsx b/stories/data-table/row-data.tsx index 53687958aa..24e8b63867 100644 --- a/stories/data-table/row-data.tsx +++ b/stories/data-table/row-data.tsx @@ -28,12 +28,12 @@ export const DATA_HEADERS = [ { fieldKey: "status", label: "Status", - keyFieldLabel: true, + keyColumn: true, }, { fieldKey: "resource", label: "Resource", - keyFieldLabel: true, + keyColumn: true, }, { fieldKey: "time", From a30ec098697501fa6dbf22cf8ec71c54fe610f9f Mon Sep 17 00:00:00 2001 From: benjaminLeongSK Date: Tue, 14 Apr 2026 17:46:45 +0800 Subject: [PATCH 10/16] [#1073][BL] Refactor logic --- src/data-table/data-table.tsx | 141 ++++++++++++---------------------- 1 file changed, 49 insertions(+), 92 deletions(-) diff --git a/src/data-table/data-table.tsx b/src/data-table/data-table.tsx index 46c0cbab1f..75de90470e 100644 --- a/src/data-table/data-table.tsx +++ b/src/data-table/data-table.tsx @@ -1,18 +1,10 @@ import { ArrowDownIcon, ArrowUpIcon } from "@lifesg/react-icons"; -import { - ReactNode, - isValidElement, - useCallback, - useEffect, - useRef, - useState, -} from "react"; +import { useCallback, useEffect, useRef, useState } from "react"; import { useInView } from "react-intersection-observer"; import { useResizeDetector } from "react-resize-detector"; import { LoadingDotsSpinner } from "../animations"; import { Checkbox } from "../checkbox"; import { ErrorDisplay } from "../error-display"; -import { VisuallyHidden } from "../shared/accessibility"; import { Typography } from "../typography"; import { useEventListener } from "../util/use-event-listener"; import { @@ -36,6 +28,7 @@ import { TextButton, } from "./data-table.styles"; import { DataTableProps, HeaderProps, RowProps } from "./types"; +import { VisuallyHidden, concatIds } from "../shared/accessibility"; export const DataTable = ({ id, @@ -185,59 +178,26 @@ export const DataTable = ({ return `Sort ${label} by ${nextSortDirection} order`; }; - const extractTextFromNode = (node: ReactNode): string => { - if (node === null || node === undefined || typeof node === "boolean") { - return ""; - } - - if (typeof node === "string" || typeof node === "number") { - return node.toString(); - } - - if (Array.isArray(node)) { - return node.map(extractTextFromNode).join(" ").trim(); - } - - if (isValidElement(node)) { - return extractTextFromNode(node.props.children); - } - - return ""; + const getRowPositionId = (rowId: string) => { + return `${id || "table"}-row-${rowId}-position`; }; - const getKeyFieldText = (header: HeaderProps, row: RowProps): string => { - if (typeof header === "string") { - return ""; - } - - if (!header.keyColumn) { - return ""; - } - - const cellData = row[header.fieldKey]; - - if (typeof cellData === "function") { - return extractTextFromNode( - cellData(row, { - isSelected: isRowSelected(row.id.toString()), - }) - ); - } - - return extractTextFromNode(cellData); + const getKeyColumnCellId = (rowId: string, fieldKey: string) => { + return `${id || "table"}-row-${rowId}-${fieldKey}-key-column`; }; - const getRowCheckboxAriaLabel = (row: RowProps, index: number): string => { - const keyFieldText = headers - .map((header) => getKeyFieldText(header, row)) - .filter((value) => value.length > 0) - .join(". "); + const getRowCheckboxAriaLabelledBy = (rowId: string) => { + const keyColumnIds = headers + .map((header) => { + if (typeof header === "string" || !header.keyColumn) { + return undefined; + } - const rowPositionText = `Row ${index + 1} of ${rows?.length ?? 0}`; + return getKeyColumnCellId(rowId, header.fieldKey); + }) + .filter((value): value is string => !!value); - return keyFieldText - ? `${rowPositionText}. ${keyFieldText}` - : rowPositionText; + return concatIds(getRowPositionId(rowId), ...keyColumnIds); }; const calculateFixedInViewport = () => { @@ -330,42 +290,30 @@ export const DataTable = ({ style={style} $isCheckbox={false} > - {clickable ? ( - <> - onHeaderClick?.(fieldKey)} - /> - - {typeof label === "string" ? ( - - {label} - - ) : ( - label - )} - {renderSortedArrow(fieldKey)} - - - ) : ( - - {typeof label === "string" ? ( - - {label} - - ) : ( - label - )} - {renderSortedArrow(fieldKey)} - + : label + : undefined + } + onClick={() => onHeaderClick?.(fieldKey)} + /> )} + + + {typeof label === "string" ? ( + + {label} + + ) : ( + label + )} + {renderSortedArrow(fieldKey)} + ); }; @@ -448,6 +396,7 @@ export const DataTable = ({ const renderRowCell = (header: HeaderProps, row: RowProps) => { const style = typeof header !== "string" ? header.style : undefined; const fieldKey = typeof header === "string" ? header : header.fieldKey; + const isKeyColumn = typeof header !== "string" && !!header.keyColumn; const rowId = row.id.toString(); const cellData = row[fieldKey]; const cellId = `${rowId}-${fieldKey}`; @@ -456,6 +405,11 @@ export const DataTable = ({ @@ -480,16 +434,19 @@ export const DataTable = ({ $isCheckbox={true} > + + {`Row ${index + 1} of ${rows?.length ?? 0}`} + { if (onSelect) { onSelect(rowId, !isRowSelected(rowId)); } }} disabled={isDisabledRow(rowId)} - aria-disabled={isDisabledRow(rowId)} + focusableWhenDisabled={isDisabledRow(rowId)} /> From 957089651c28473fb0bc85f29a22f972a96c6f76 Mon Sep 17 00:00:00 2001 From: benjaminLeongSK Date: Tue, 14 Apr 2026 22:58:59 +0800 Subject: [PATCH 11/16] [#1073][BL] Replace ClickableHeader component --- src/data-table/data-table.styles.tsx | 18 +++++------------- src/data-table/data-table.tsx | 28 ++++++++++++++++------------ 2 files changed, 21 insertions(+), 25 deletions(-) diff --git a/src/data-table/data-table.styles.tsx b/src/data-table/data-table.styles.tsx index d633e74d53..19ced5d7e1 100644 --- a/src/data-table/data-table.styles.tsx +++ b/src/data-table/data-table.styles.tsx @@ -199,6 +199,11 @@ export const HeaderCell = styled.th` `; } }}; + &:focus-within { + outline: 2px solid ${Colour["focus-ring"]}; + outline-offset: -3px; + z-index: 1; + } `; export const HeaderCellWrapper = styled.div` @@ -214,19 +219,6 @@ export const HeaderCellWrapper = styled.div` } `; -export const ClickableHeader = styled(BasicButton)` - position: absolute; - inset: 0.25rem; - z-index: 2; - cursor: pointer; - border-radius: ${Radius["sm"]}; - - &:focus-visible { - outline: 2px solid ${Colour["focus-ring"]}; - outline-offset: 0; - } -`; - export const BodyRow = styled.tr` background-color: ${(props) => { if (props.$isSelected) { diff --git a/src/data-table/data-table.tsx b/src/data-table/data-table.tsx index 75de90470e..6ec50f65bc 100644 --- a/src/data-table/data-table.tsx +++ b/src/data-table/data-table.tsx @@ -14,7 +14,6 @@ import { BodyCellContent, BodyRow, CheckBoxWrapper, - ClickableHeader, EmptyViewCell, ErrorDisplayTitle, HeaderCell, @@ -291,17 +290,22 @@ export const DataTable = ({ $isCheckbox={false} > {(clickable || isSortable) && ( - onHeaderClick?.(fieldKey)} - /> + + + + )} ); }; @@ -360,7 +343,7 @@ export const DataTable = ({ { if (onSelectAll) { onSelectAll(isAllCheckboxSelected()); @@ -392,7 +375,7 @@ export const DataTable = ({ $isSelectable={enableMultiSelect} $isSelected={isRowSelected(row.id.toString())} > - {enableMultiSelect && renderRowCheckBox(row, index)} + {enableMultiSelect && renderRowCheckBox(row)} {headers.map((header) => renderRowCell(header, row))} @@ -412,7 +395,7 @@ export const DataTable = ({ @@ -428,7 +411,7 @@ export const DataTable = ({ ); }; - const renderRowCheckBox = (row: RowProps, index: number) => { + const renderRowCheckBox = (row: RowProps) => { const rowId = row.id.toString(); return ( @@ -437,9 +420,6 @@ export const DataTable = ({ $isCheckbox={true} > - - {`Row ${index + 1} of ${rows?.length ?? 0}`} - Date: Tue, 21 Apr 2026 10:46:45 +0800 Subject: [PATCH 15/16] [#1073][RL] Hide decorative icon from screen readers --- src/data-table/data-table.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/data-table/data-table.tsx b/src/data-table/data-table.tsx index a44fa520c3..4069474135 100644 --- a/src/data-table/data-table.tsx +++ b/src/data-table/data-table.tsx @@ -318,12 +318,14 @@ export const DataTable = ({ if (isSorted === "asc") { return ( ); } return ( ); From a0886724d4dca6e11085420316e744d84356f618 Mon Sep 17 00:00:00 2001 From: Quek Ruo Ling Date: Tue, 21 Apr 2026 10:52:22 +0800 Subject: [PATCH 16/16] [#1073][RL] Remove unnecessary style --- src/data-table/data-table.styles.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/data-table/data-table.styles.tsx b/src/data-table/data-table.styles.tsx index 18157b7d54..c8999d5663 100644 --- a/src/data-table/data-table.styles.tsx +++ b/src/data-table/data-table.styles.tsx @@ -184,7 +184,6 @@ export const HeaderRow = styled.tr` `; export const HeaderCell = styled.th` - position: relative; padding: ${(props) => props.$isCheckbox ? "1.25rem 0.5rem 1.25rem 1.5rem" : "1.25rem 1rem"}; text-align: left;