React table components for PatternFly design system with sorting, selection, expansion, and editing capabilities
—
Quality
Pending
Does it follow best practices?
Impact
Pending
No eval scenarios have been run
Inline editing components for text and select inputs with validation support and row-level editing controls.
Inline editable text cell component with validation support.
/**
* Inline editable text cell component
* @param props - EditableTextCell configuration props
* @returns EditableTextCell component
*/
function EditableTextCell(props: IEditableTextCell): React.FunctionComponent<IEditableTextCell>;
interface IEditableTextCell extends React.HTMLProps<HTMLDivElement> {
/** The current value of the text input */
value: string;
/** Row index of this text cell */
rowIndex: number;
/** Cell index of this text cell */
cellIndex: number;
/** Props to build the input */
props: EditableTextCellProps;
/** Event handler which fires when user changes the text in this cell */
handleTextInputChange: (
newValue: string,
event: React.FormEvent<HTMLInputElement>,
rowIndex: number,
cellIndex: number
) => void;
/** Accessible label of the text input */
inputAriaLabel: string;
/** Flag indicating if the text input is disabled */
isDisabled?: boolean;
}
interface EditableTextCellProps {
/** Name of the input */
name: string;
/** Value to display in the cell */
value: string;
/** Editable value (can differ from display value) */
editableValue?: string;
/** Whether the cell is valid */
isValid?: boolean;
/** Error text to display */
errorText?: string;
/** Arbitrary data to pass to the internal text input */
[key: string]: any;
}Usage Examples:
import { EditableTextCell } from "@patternfly/react-table";
// Basic editable text cell
<Td>
<EditableTextCell
value={rowData.name}
rowIndex={rowIndex}
cellIndex={0}
props={{
name: 'name',
value: rowData.name,
editableValue: editValues[`${rowIndex}-0`],
isValid: validationResults[`${rowIndex}-0`]?.isValid !== false
}}
handleTextInputChange={(newValue, event, rowIndex, cellIndex) => {
setEditValues(prev => ({
...prev,
[`${rowIndex}-${cellIndex}`]: newValue
}));
validateCell(rowIndex, cellIndex, newValue);
}}
inputAriaLabel={`Edit name for row ${rowIndex}`}
/>
</Td>
// Editable text cell with validation error
<Td>
<EditableTextCell
value={rowData.email}
rowIndex={rowIndex}
cellIndex={1}
props={{
name: 'email',
value: rowData.email,
editableValue: editValues[`${rowIndex}-1`],
isValid: false,
errorText: 'Please enter a valid email address',
}}
handleTextInputChange={handleEmailChange}
inputAriaLabel={`Edit email for row ${rowIndex}`}
isDisabled={!isEditing}
/>
</Td>Inline editable select cell component with support for single and multi-select.
/**
* Inline editable select cell component
* @param props - EditableSelectInputCell configuration props
* @returns EditableSelectInputCell component
*/
function EditableSelectInputCell(props: IEditableSelectInputCell): React.FunctionComponent<IEditableSelectInputCell>;
interface IEditableSelectInputCell extends Omit<React.HTMLProps<HTMLElement | HTMLDivElement>, 'onSelect' | 'onToggle'> {
/** Row index of this select input cell */
rowIndex: number;
/** Cell index of this select input cell */
cellIndex: number;
/** Props to build the select component */
props: EditableSelectInputProps;
/** Event handler which fires when user selects an option in this cell */
onSelect: (
event: React.MouseEvent | React.ChangeEvent,
newValue: any | any[],
rowIndex: number,
cellIndex: number,
isPlaceholder?: boolean
) => void;
/** Options to display in the expandable select menu */
options?: React.ReactElement<any>[];
/** Flag indicating the select input is disabled */
isDisabled?: boolean;
/** Flag indicating the toggle gets placeholder styles */
isPlaceholder?: boolean;
/** Current selected options to display as the read only value of the table cell */
selections?: any | any[];
/** Flag indicating the select menu is open */
isOpen?: boolean;
/** Event handler which fires when the select toggle is toggled */
onToggle?: (event: React.MouseEvent | undefined) => void;
/** Event handler which fires when the user clears the selections */
clearSelection?: (event: React.MouseEvent, rowIndex: number, cellIndex: number) => void;
}
interface EditableSelectInputProps {
/** Name of the select input */
name: string;
/** Value to display in the cell */
value: string | string[];
/** Flag controlling isOpen state of select */
isSelectOpen: boolean;
/** Single select option value for single select menus, or array for multi select */
selected: any | any[];
/** Array of react elements to display in the select menu */
options: React.ReactElement<any>[];
/** Props to be passed down to the select component */
editableSelectProps?: SelectProps;
/** Error text to display */
errorText?: string;
/** Arbitrary data to pass to the internal select component */
[key: string]: any;
}Usage Examples:
import { EditableSelectInputCell, SelectOption } from "@patternfly/react-table";
// Basic editable select cell
const statusOptions = [
<SelectOption key="active" value="active">Active</SelectOption>,
<SelectOption key="inactive" value="inactive">Inactive</SelectOption>,
<SelectOption key="pending" value="pending">Pending</SelectOption>
];
<Td>
<EditableSelectInputCell
rowIndex={rowIndex}
cellIndex={2}
props={{
name: 'status',
value: rowData.status,
isSelectOpen: openSelects[`${rowIndex}-2`] || false,
selected: editValues[`${rowIndex}-2`] || rowData.status,
options: statusOptions
}}
options={statusOptions}
selections={editValues[`${rowIndex}-2`] || rowData.status}
isOpen={openSelects[`${rowIndex}-2`] || false}
onSelect={(event, value, rowIndex, cellIndex) => {
setEditValues(prev => ({
...prev,
[`${rowIndex}-${cellIndex}`]: value
}));
setOpenSelects(prev => ({
...prev,
[`${rowIndex}-${cellIndex}`]: false
}));
}}
onToggle={(event) => {
setOpenSelects(prev => ({
...prev,
[`${rowIndex}-2`]: !prev[`${rowIndex}-2`]
}));
}}
/>
</Td>
// Multi-select editable cell
<Td>
<EditableSelectInputCell
rowIndex={rowIndex}
cellIndex={3}
props={{
name: 'tags',
value: rowData.tags,
isSelectOpen: openSelects[`${rowIndex}-3`] || false,
selected: editValues[`${rowIndex}-3`] || rowData.tags,
options: tagOptions,
editableSelectProps: { variant: 'checkbox' }
}}
options={tagOptions}
selections={editValues[`${rowIndex}-3`] || rowData.tags}
isOpen={openSelects[`${rowIndex}-3`] || false}
onSelect={handleMultiSelect}
onToggle={toggleMultiSelect}
clearSelection={(event, rowIndex, cellIndex) => {
setEditValues(prev => ({
...prev,
[`${rowIndex}-${cellIndex}`]: []
}));
}}
/>
</Td>// Row editing event handler
type OnRowEdit = (
event: React.MouseEvent<HTMLButtonElement>,
type: RowEditType,
isEditable?: boolean,
rowIndex?: number,
validationErrors?: RowErrors
) => void;
// Row edit action types
type RowEditType = 'save' | 'cancel' | 'edit';
// Row validation errors
interface RowErrors {
[name: string]: string[];
}
// Row validation definition
interface IValidatorDef {
validator: (value: string) => boolean;
errorText: string;
name: string;
}// IRow interface includes editing properties
interface IRow extends RowType {
/** Whether the row is editable */
isEditable?: boolean;
/** Whether the row is valid */
isValid?: boolean;
/** Array of validation functions to run against every cell for a given row */
rowEditValidationRules?: IValidatorDef[];
/** Aria label for edit button in inline edit */
rowEditBtnAriaLabel?: (idx: number) => string;
/** Aria label for save button in inline edit */
rowSaveBtnAriaLabel?: (idx: number) => string;
/** Aria label for cancel button in inline edit */
rowCancelBtnAriaLabel?: (idx: number) => string;
}Usage Examples:
// Row with editing configuration
const editableRows = [
{
cells: ['John Doe', 'john@example.com', 'Active'],
isEditable: editingRows.includes(0),
isValid: rowValidation[0]?.isValid,
rowEditValidationRules: [
{
name: 'email',
validator: (value) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value),
errorText: 'Please enter a valid email address'
},
{
name: 'name',
validator: (value) => value.trim().length > 0,
errorText: 'Name is required'
}
],
rowEditBtnAriaLabel: (idx) => `Edit row ${idx}`,
rowSaveBtnAriaLabel: (idx) => `Save changes for row ${idx}`,
rowCancelBtnAriaLabel: (idx) => `Cancel editing row ${idx}`
}
];
// Row edit handlers
const handleRowEdit = (event, type, isEditable, rowIndex, validationErrors) => {
switch (type) {
case 'edit':
setEditingRows(prev => [...prev, rowIndex]);
break;
case 'save':
if (!validationErrors || Object.keys(validationErrors).length === 0) {
// Save the changes
saveRowChanges(rowIndex);
setEditingRows(prev => prev.filter(idx => idx !== rowIndex));
}
break;
case 'cancel':
// Discard changes
discardRowChanges(rowIndex);
setEditingRows(prev => prev.filter(idx => idx !== rowIndex));
break;
}
};// Validation function for individual cells
const validateCell = (rowIndex: number, cellIndex: number, value: string): boolean => {
const row = rows[rowIndex];
if (row.rowEditValidationRules) {
const rule = row.rowEditValidationRules[cellIndex];
if (rule && !rule.validator(value)) {
setValidationErrors(prev => ({
...prev,
[`${rowIndex}-${cellIndex}`]: rule.errorText
}));
return false;
}
}
// Clear validation error if valid
setValidationErrors(prev => {
const newErrors = { ...prev };
delete newErrors[`${rowIndex}-${cellIndex}`];
return newErrors;
});
return true;
};// Validation function for entire rows
const validateRow = (rowIndex: number): RowErrors | null => {
const row = rows[rowIndex];
const errors: RowErrors = {};
if (row.rowEditValidationRules) {
row.rowEditValidationRules.forEach((rule, cellIndex) => {
const cellValue = getCellValue(rowIndex, cellIndex);
if (!rule.validator(cellValue)) {
if (!errors[rule.name]) {
errors[rule.name] = [];
}
errors[rule.name].push(rule.errorText);
}
});
}
return Object.keys(errors).length > 0 ? errors : null;
};// Example state management for table editing
const [editingRows, setEditingRows] = useState<number[]>([]);
const [editValues, setEditValues] = useState<Record<string, any>>({});
const [openSelects, setOpenSelects] = useState<Record<string, boolean>>({});
const [validationErrors, setValidationErrors] = useState<Record<string, string>>({});
// Helper functions
const startEditing = (rowIndex: number) => {
setEditingRows(prev => [...prev, rowIndex]);
// Initialize edit values with current row values
const row = rows[rowIndex];
row.cells.forEach((cell, cellIndex) => {
setEditValues(prev => ({
...prev,
[`${rowIndex}-${cellIndex}`]: cell
}));
});
};
const saveChanges = (rowIndex: number) => {
const validationErrors = validateRow(rowIndex);
if (!validationErrors) {
// Apply changes to the row data
const updatedRows = [...rows];
updatedRows[rowIndex].cells = updatedRows[rowIndex].cells.map((_, cellIndex) =>
editValues[`${rowIndex}-${cellIndex}`] || _
);
setRows(updatedRows);
setEditingRows(prev => prev.filter(idx => idx !== rowIndex));
clearEditState(rowIndex);
}
};
const cancelEditing = (rowIndex: number) => {
setEditingRows(prev => prev.filter(idx => idx !== rowIndex));
clearEditState(rowIndex);
};
const clearEditState = (rowIndex: number) => {
// Clear edit values for this row
const keysToRemove = Object.keys(editValues).filter(key =>
key.startsWith(`${rowIndex}-`)
);
setEditValues(prev => {
const newState = { ...prev };
keysToRemove.forEach(key => delete newState[key]);
return newState;
});
// Clear validation errors for this row
const errorKeysToRemove = Object.keys(validationErrors).filter(key =>
key.startsWith(`${rowIndex}-`)
);
setValidationErrors(prev => {
const newState = { ...prev };
errorKeysToRemove.forEach(key => delete newState[key]);
return newState;
});
};Install with Tessl CLI
npx tessl i tessl/npm-patternfly--react-table