Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
101 changes: 101 additions & 0 deletions packages/react-data-grid-react-window/DISABLED_ROWS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
# Disabled Row Support

This package now supports disabled rows in the DataGrid component. Disabled rows provide a way to display data that should be non-interactive and visually distinct.

## Features

- βœ… **Visual Distinction**: Disabled rows are grayed out with reduced opacity
- βœ… **Selection Exclusion**: Disabled rows cannot be selected and are excluded from "select all" operations
- βœ… **Hidden Selection UI**: Selection checkboxes are hidden for disabled rows
- βœ… **Automatic Positioning**: Disabled rows are automatically moved to the end, even when sorting
- βœ… **Accessibility**: Proper `aria-disabled` attributes are applied

## Usage

### Basic Usage

```tsx
import { DataGrid, WithDisabled } from '@fluentui-contrib/react-data-grid-react-window';

// Define your item type with disabled support
type MyItem = WithDisabled<{
id: number;
name: string;
value: string;
}>;

// Create items with some disabled
const items: MyItem[] = [
{ id: 1, name: 'Item 1', value: 'Active', disabled: false },
{ id: 2, name: 'Item 2', value: 'Disabled', disabled: true },
{ id: 3, name: 'Item 3', value: 'Active' }, // disabled is optional
];

// Use in DataGrid as normal
<DataGrid
items={items}
columns={columns}
selectionMode="multiselect"
>
{/* Your DataGrid content */}
</DataGrid>
```

### Advanced Usage

```tsx
// You can also use the DisabledItem interface directly
import { DisabledItem } from '@fluentui-contrib/react-data-grid-react-window';

interface CustomItem extends DisabledItem {
id: number;
name: string;
status: 'active' | 'inactive' | 'archived';
}

const items: CustomItem[] = [
{ id: 1, name: 'Item 1', status: 'active' },
{ id: 2, name: 'Item 2', status: 'archived', disabled: true },
];
```

## API

### Types

#### `DisabledItem`
```tsx
interface DisabledItem {
disabled?: boolean;
}
```

#### `WithDisabled<T>`
```tsx
type WithDisabled<T> = T & DisabledItem;
```

A helper type that adds disabled support to any item type.

### Behavior

1. **Sorting**: When columns are sorted, disabled items are automatically moved to the end while preserving the sort order of enabled items.

2. **Selection**:
- Disabled rows cannot be individually selected
- "Select all" operations exclude disabled rows
- Selection callbacks receive filtered results without disabled items

3. **Visual State**:
- Disabled rows have reduced opacity
- Selection checkboxes are hidden
- Hover and focus states are disabled
- Proper color tokens for accessibility

4. **Accessibility**:
- `aria-disabled="true"` is applied to disabled rows
- Screen readers will announce the disabled state

## Examples

See the `DisabledRows` story in Storybook for a complete working example that demonstrates all features including sorting and selection behavior.
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,26 @@ import {
} from '@fluentui/react-components';
import { useFluent, useScrollbarWidth } from '@fluentui/react-components';
import { DataGridState } from './DataGrid.types';
import type { DisabledItem } from '../../types';

const TABLE_SELECTION_CELL_WIDTH = 44;

/**
* Sorts rows to keep disabled items at the end while preserving the current sort order
* This should be applied after the base sorting logic
*/
const moveDisabledRowsToEnd = <T>(rows: T[] | undefined): T[] => {
// Handle undefined/null rows gracefully
if (!rows || !Array.isArray(rows)) {
return [];
}

// Assuming rows have an 'item' property that contains the actual data
const enabledRows = rows.filter((row: any) => !(row.item as DisabledItem)?.disabled);
const disabledRows = rows.filter((row: any) => (row.item as DisabledItem)?.disabled);
return [...enabledRows, ...disabledRows];
};

/**
* Create the state required to render DataGrid.
*
Expand All @@ -26,6 +43,34 @@ export const useDataGrid_unstable = (
const headerRef = React.useRef<HTMLDivElement | null>(null);
const bodyRef = React.useRef<HTMLDivElement | null>(null);

// Override selection change callback to exclude disabled items
const originalOnSelectionChange = props.onSelectionChange;
const onSelectionChange = React.useCallback((e: any, data: any) => {
if (originalOnSelectionChange) {
// Filter selection to only include enabled items
const filteredData = {
...data,
selectedItems: data.selectedItems?.filter((item: any) =>
!(item as DisabledItem).disabled
) || [],
};
originalOnSelectionChange(e, filteredData);
}
}, [originalOnSelectionChange]);

// Override getRowId to ensure disabled items are handled properly
const originalGetRowId = props.getRowId;
const getRowId = React.useCallback((item: any) => {
// Use original getRowId if provided, otherwise use index
if (originalGetRowId) {
return originalGetRowId(item);
}
// Fallback to item index or a generated ID
const items = props.items as DisabledItem[];
const index = items.indexOf(item);
return `row-${index}`;
}, [originalGetRowId, props.items]);

let containerWidthOffset = props.containerWidthOffset;

if (containerWidthOffset === undefined) {
Expand All @@ -36,10 +81,23 @@ export const useDataGrid_unstable = (
}

const baseState = useBaseState(
{ ...props, 'aria-rowcount': props.items.length, containerWidthOffset },
{
...props,
onSelectionChange,
getRowId,
'aria-rowcount': props.items.length,
containerWidthOffset
},
ref
);

// After the base state is created, reorder the rows to move disabled items to the end
// This preserves any sorting that was applied by the base component
const rowsWithDisabledAtEnd = React.useMemo(() => {
// Only process rows if they exist, otherwise return undefined to maintain original behavior
return baseState.rows ? moveDisabledRowsToEnd(baseState.rows) : baseState.rows;
}, [baseState.rows]);

if (
props.resizableColumns &&
props.resizableColumnsOptions?.autoFitColumns === false &&
Expand All @@ -50,6 +108,7 @@ export const useDataGrid_unstable = (

return {
...baseState,
rows: rowsWithDisabledAtEnd,
headerRef,
bodyRef,
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,10 @@ import {
TableRowIdContextProvider,
} from '@fluentui/react-components';
import { TableRowIndexContextProvider } from '../../contexts/rowIndexContext';
import { DisabledRowContextProvider } from '../../contexts/disabledRowContext';
import { useBodyRefContext } from '../../contexts/bodyRefContext';
import { useHeaderRefContext } from '../../contexts/headerRefContext';
import type { DisabledItem } from '../../types';

type RowRenderFunction =
import('@fluentui/react-components').DataGridBodyState['renderRow'];
Expand Down Expand Up @@ -50,10 +52,15 @@ export const useDataGridBody_unstable = (
const virtualizedRow: DataGridBodyState['virtualizedRow'] = React.useCallback(
({ data, index, style, isScrolling }) => {
const row: TableRowData<unknown> = data[index];
const item = row.item as DisabledItem;
const isDisabled = Boolean(item?.disabled);

return (
<TableRowIndexContextProvider value={ariaRowIndexStart + index}>
<TableRowIdContextProvider value={row.rowId}>
{children(row, style, index, isScrolling)}
<DisabledRowContextProvider value={isDisabled}>
{children(row, style, index, isScrolling)}
</DisabledRowContextProvider>
</TableRowIdContextProvider>
</TableRowIndexContextProvider>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,49 @@ import {
mergeClasses,
useDataGridRowStyles_unstable as useDataGridRowStylesBase_unstable,
DataGridRowState,
tokens,
} from '@fluentui/react-components';
import { useDisabledRowContext } from '../../contexts/disabledRowContext';

const useStyles = makeStyles({
root: {
minWidth: 'fit-content',
},
disabled: {
opacity: '0.6',
cursor: 'not-allowed',
color: tokens.colorNeutralForegroundDisabled,
backgroundColor: tokens.colorNeutralBackground2,

// Style all cells in disabled rows
'& [role="gridcell"]': {
color: tokens.colorNeutralForegroundDisabled,
},

// Hide the selection cell for disabled rows
// This targets the checkbox/selection cell specifically
'& [role="gridcell"]:first-child': {
'& input[type="checkbox"]': {
visibility: 'hidden',
},
'& [data-testid="checkbox"]': {
visibility: 'hidden',
},
},

// Disable hover and focus states for disabled rows
'&:hover': {
backgroundColor: tokens.colorNeutralBackground2,
},

'&:focus': {
outline: 'none',
},

'&[aria-selected="true"]': {
backgroundColor: tokens.colorNeutralBackground2,
},
},
});

/**
Expand All @@ -18,7 +55,13 @@ export const useDataGridRowStyles_unstable = (
state: DataGridRowState
): DataGridRowState => {
const classes = useStyles();
state.root.className = mergeClasses(classes.root, state.root.className);
const isDisabled = useDisabledRowContext();

state.root.className = mergeClasses(
classes.root,
isDisabled && classes.disabled,
state.root.className
);

useDataGridRowStylesBase_unstable(state);
return state;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import type {
} from '@fluentui/react-components';
import { useDataGridRow_unstable as useBaseState } from '@fluentui/react-components';
import { useTableRowIndexContext } from '../../contexts/rowIndexContext';
import { useDisabledRowContext } from '../../contexts/disabledRowContext';

/**
* Create the state required to render DataGridRow.
Expand All @@ -20,5 +21,54 @@ export const useDataGridRow_unstable = (
ref: React.Ref<HTMLElement>
): DataGridRowState => {
const rowIndex = useTableRowIndexContext();
return useBaseState({ ...props, 'aria-rowindex': rowIndex }, ref);
const isDisabled = useDisabledRowContext();

// Override onClick to prevent selection on disabled rows
const originalOnClick = props.onClick;
const onClick = React.useCallback((e: React.MouseEvent<HTMLElement>) => {
if (isDisabled) {
e.preventDefault();
e.stopPropagation();
return;
}
if (originalOnClick) {
originalOnClick(e);
}
}, [isDisabled, originalOnClick]);

// Override onKeyDown to prevent keyboard selection on disabled rows
const originalOnKeyDown = props.onKeyDown;
const onKeyDown = React.useCallback((e: React.KeyboardEvent<HTMLElement>) => {
if (isDisabled && (e.key === ' ' || e.key === 'Enter')) {
e.preventDefault();
e.stopPropagation();
return;
}
if (originalOnKeyDown) {
originalOnKeyDown(e);
}
}, [isDisabled, originalOnKeyDown]);

const state = useBaseState({
...props,
onClick,
onKeyDown,
'aria-rowindex': rowIndex
}, ref);

// Add disabled state to the row for accessibility and styling
if (isDisabled) {
// Set aria-disabled for accessibility
if (state.root) {
state.root['aria-disabled'] = true;
// Disable selection by setting aria-selected to false and making it non-selectable
state.root['aria-selected'] = false;
// Remove any selection-related attributes to prevent selection
if (state.root.tabIndex !== undefined) {
state.root.tabIndex = -1;
}
}
}

return state;
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import * as React from 'react';

const disabledRowContext = React.createContext<boolean | undefined>(undefined);

export const disabledRowContextDefaultValue = false;

export const useDisabledRowContext = () =>
React.useContext(disabledRowContext) ?? disabledRowContextDefaultValue;

export const DisabledRowContextProvider = disabledRowContext.Provider;
1 change: 1 addition & 0 deletions packages/react-data-grid-react-window/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,3 +41,4 @@ export type {
RowRenderer,
} from './components/DataGridBody';
export type { DataGridState } from './components/DataGrid';
export type { DisabledItem, WithDisabled } from './types';
14 changes: 14 additions & 0 deletions packages/react-data-grid-react-window/src/types/DisabledItem.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
/**
* Represents an item that can be disabled in the DataGrid
*/
export interface DisabledItem {
/**
* Whether this item should be disabled (non-selectable and positioned at the end)
*/
disabled?: boolean;
}

/**
* Helper type to create an item type that supports disabled state
*/
export type WithDisabled<T> = T & DisabledItem;
1 change: 1 addition & 0 deletions packages/react-data-grid-react-window/src/types/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export type { DisabledItem, WithDisabled } from './DisabledItem';
Loading