diff --git a/packages/react-aria-components/src/Table.tsx b/packages/react-aria-components/src/Table.tsx index b4c8c9f3b8c..7dc92b04c5f 100644 --- a/packages/react-aria-components/src/Table.tsx +++ b/packages/react-aria-components/src/Table.tsx @@ -615,6 +615,11 @@ export interface TableProps * the Table. */ dragAndDropHooks?: DragAndDropHooks; + /** + * An explicit id to use for the select all checkbox so it remains stable across renders and + * refreshes. + */ + rowHeaderCustomColumnId?: string; } /** diff --git a/packages/react-aria/src/grid/useGridSelectionCheckbox.ts b/packages/react-aria/src/grid/useGridSelectionCheckbox.ts index 8aa81273213..7f7ab05a017 100644 --- a/packages/react-aria/src/grid/useGridSelectionCheckbox.ts +++ b/packages/react-aria/src/grid/useGridSelectionCheckbox.ts @@ -9,6 +9,8 @@ import {useLocalizedStringFormatter} from '../i18n/useLocalizedStringFormatter'; export interface AriaGridSelectionCheckboxProps { /** A unique key for the checkbox. */ key: Key; + /** An explicit id to use for the checkbox element. */ + id?: string; } export interface GridSelectionCheckboxAria { @@ -26,7 +28,7 @@ export function useGridSelectionCheckbox>( props: AriaGridSelectionCheckboxProps, state: GridState ): GridSelectionCheckboxAria { - let {key} = props; + let {key, id} = props; let manager = state.selectionManager; let checkboxId = useId(); @@ -40,7 +42,7 @@ export function useGridSelectionCheckbox>( return { checkboxProps: { - id: checkboxId, + id: id ?? checkboxId, 'aria-label': stringFormatter.format('select'), isSelected, isDisabled, diff --git a/packages/react-aria/src/table/useTableSelectionCheckbox.ts b/packages/react-aria/src/table/useTableSelectionCheckbox.ts index 0c9805752a5..bd1b8386df9 100644 --- a/packages/react-aria/src/table/useTableSelectionCheckbox.ts +++ b/packages/react-aria/src/table/useTableSelectionCheckbox.ts @@ -21,6 +21,8 @@ import {useLocalizedStringFormatter} from '../i18n/useLocalizedStringFormatter'; export interface AriaTableSelectionCheckboxProps { /** A unique key for the checkbox. */ key: Key; + /** An explicit id to use for the checkbox element. */ + id?: string; } export interface TableSelectionCheckboxAria { @@ -43,8 +45,8 @@ export function useTableSelectionCheckbox( props: AriaTableSelectionCheckboxProps, state: TableState ): TableSelectionCheckboxAria { - let {key} = props; - const {checkboxProps} = useGridSelectionCheckbox(props, state); + let {key, id} = props; + const {checkboxProps} = useGridSelectionCheckbox({...props, id}, state); return { checkboxProps: { @@ -64,8 +66,11 @@ export function useTableSelectAllCheckbox(state: TableState): TableSelectA let {isEmpty, isSelectAll, selectionMode} = state.selectionManager; const stringFormatter = useLocalizedStringFormatter(intlMessages, '@react-aria/table'); + let selectionColumn = state.collection.columns.find(column => column.props?.isSelectionCell); + return { checkboxProps: { + id: selectionColumn?.key?.toString(), 'aria-label': stringFormatter.format(selectionMode === 'single' ? 'select' : 'selectAll'), isSelected: isSelectAll, isDisabled: diff --git a/packages/react-aria/test/table/useTable.test.tsx b/packages/react-aria/test/table/useTable.test.tsx index 1211870d45b..67bd0b08718 100644 --- a/packages/react-aria/test/table/useTable.test.tsx +++ b/packages/react-aria/test/table/useTable.test.tsx @@ -157,5 +157,23 @@ describe('useTable', () => { ); expect(getByTestId('test-id').id).toEqual('table-id'); }); + + it('uses a custom id for the select all checkbox', () => { + let {container} = render( + + + {column => {column.name}} + + + {item => {columnKey => {item[columnKey]}}} + +
+ ); + + expect(container.querySelector('#custom-select-all-id')).toBeTruthy(); + }); }); }); diff --git a/packages/react-stately/src/table/TableCollection.ts b/packages/react-stately/src/table/TableCollection.ts index d253f6ff35f..884ab9523e4 100644 --- a/packages/react-stately/src/table/TableCollection.ts +++ b/packages/react-stately/src/table/TableCollection.ts @@ -34,6 +34,7 @@ export interface ITableCollection extends IGridCollection { interface GridCollectionOptions { showSelectionCheckboxes?: boolean; showDragButtons?: boolean; + rowHeaderCustomColumnId?: string; } const ROW_HEADER_COLUMN_KEY = 'row-header-column-' + Math.random().toString(36).slice(2); @@ -218,7 +219,7 @@ export class TableCollection extends GridCollection implements ITableColle if (opts?.showSelectionCheckboxes) { let rowHeaderColumn: GridNode = { type: 'column', - key: ROW_HEADER_COLUMN_KEY, + key: opts?.rowHeaderCustomColumnId || ROW_HEADER_COLUMN_KEY, value: null, textValue: '', level: 0, diff --git a/packages/react-stately/src/table/useTableState.ts b/packages/react-stately/src/table/useTableState.ts index 2bc9c95db02..e6cd28eb727 100644 --- a/packages/react-stately/src/table/useTableState.ts +++ b/packages/react-stately/src/table/useTableState.ts @@ -33,6 +33,11 @@ import {useControlledState} from '../utils/useControlledState'; export interface TableProps extends MultipleSelection, Sortable, Expandable { /** The elements that make up the table. Includes the TableHeader, TableBody, Columns, and Rows. */ children: [ReactElement>, ReactElement>]; + /** + * An explicit id to use for the select all checkbox so it remains stable across renders and + * refreshes. + */ + rowHeaderCustomColumnId?: string; /** A list of row keys to disable. */ disabledKeys?: Iterable; /** @@ -90,6 +95,7 @@ export interface CollectionBuilderContext { showDragButtons: boolean; selectionMode: SelectionMode; columns: Node[]; + rowHeaderCustomColumnId?: string; } export interface TableStateProps extends MultipleSelectionStateProps, Expandable, Sortable { @@ -107,6 +113,11 @@ export interface TableStateProps extends MultipleSelectionStateProps, Expanda showDragButtons?: boolean; /** @private - Do not use unless you know what you're doing. */ UNSAFE_selectionState?: MultipleSelectionState; + /** + * An explicit id to use for the select all checkbox so it remains stable across renders and + * refreshes. + */ + rowHeaderCustomColumnId?: string; /** The id of the column that displays hierarchical data. */ treeColumn?: Key; } @@ -129,11 +140,10 @@ export function useTableState(props: TableStateProps): Tabl showSelectionCheckboxes: showSelectionCheckboxes && selectionMode !== 'none', showDragButtons: showDragButtons, selectionMode, - columns: [] + columns: [], + rowHeaderCustomColumnId: props.rowHeaderCustomColumnId }), - // eslint-disable-next-line react-hooks/exhaustive-deps - // oxlint-disable-next-line react/react-compiler, react-hooks/exhaustive-deps - [props.children, showSelectionCheckboxes, selectionMode, showDragButtons] + [props.children, showSelectionCheckboxes, selectionMode, showDragButtons, props.rowHeaderCustomColumnId] ); let collection = useCollection>(