Skip to content

Commit

Permalink
docs(Table): add Editable Table example (#10341)
Browse files Browse the repository at this point in the history
* docs(Table): add Editable Table example

* fix(TableEditable example): group imports

* fix(Table editable example): focus when using keyboard

* fix(TableEditable example): PR review updates

* fix: update aria-label, missing wrapper classes

* fix lint errors

---------

Co-authored-by: nicolethoen <[email protected]>
  • Loading branch information
adamviktora and nicolethoen authored Jul 16, 2024
1 parent f38d2ec commit 1779fcd
Show file tree
Hide file tree
Showing 2 changed files with 324 additions and 0 deletions.
11 changes: 11 additions & 0 deletions packages/react-table/src/components/Table/examples/Table.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,11 +51,15 @@ import FolderOpenIcon from '@patternfly/react-icons/dist/esm/icons/folder-open-i
import SortAmountDownIcon from '@patternfly/react-icons/dist/esm/icons/sort-amount-down-icon';
import BlueprintIcon from '@patternfly/react-icons/dist/esm/icons/blueprint-icon';
import EllipsisVIcon from '@patternfly/react-icons/dist/esm/icons/ellipsis-v-icon';
import PencilAltIcon from '@patternfly/react-icons/dist/esm/icons/pencil-alt-icon';
import CheckIcon from '@patternfly/react-icons/dist/esm/icons/check-icon';
import TimesIcon from '@patternfly/react-icons/dist/esm/icons/times-icon';

import { css } from '@patternfly/react-styles';
import styles from '@patternfly/react-styles/css/components/Table/table';
import spacing from '@patternfly/react-styles/css/utilities/Spacing/spacing';
import textStyles from '@patternfly/react-styles/css/utilities/Text/text';
import inlineEditStyles from '@patternfly/react-styles/css/components/InlineEdit/inline-edit';
import global_BackgroundColor_150 from '@patternfly/react-tokens/dist/esm/global_BackgroundColor_150';

## Table examples
Expand Down Expand Up @@ -156,6 +160,13 @@ This selectable rows feature is intended for use when a table is used to present
```ts file="TableClickable.tsx"
```

### Editable rows

This example shows a table with editable rows. Cells in a row can be edited after clicking on the edit icon.

```ts file="TableEditable.tsx"
```

### Actions

This example demonstrates adding actions as the last column. The header's last cell is an empty cell, and each body row's last cell is an action cell.
Expand Down
313 changes: 313 additions & 0 deletions packages/react-table/src/components/Table/examples/TableEditable.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,313 @@
import React from 'react';
import { Table, Thead, Tr, Th, Tbody, Td } from '@patternfly/react-table';
import { Button, Checkbox, Radio, TextInput, KeyTypes, getUniqueId } from '@patternfly/react-core';
import PencilAltIcon from '@patternfly/react-icons/dist/esm/icons/pencil-alt-icon';
import CheckIcon from '@patternfly/react-icons/dist/esm/icons/check-icon';
import TimesIcon from '@patternfly/react-icons/dist/esm/icons/times-icon';
import inlineEditStyles from '@patternfly/react-styles/css/components/InlineEdit/inline-edit';
import { css } from '@patternfly/react-styles';

interface EditButtonsCellProps {
onClick: (type: 'save' | 'cancel' | 'edit') => void;
elementToFocusOnEditRef?: React.MutableRefObject<HTMLElement>;
rowAriaLabel: string;
}

const EditButtonsCell: React.FunctionComponent<EditButtonsCellProps> = ({
onClick,
elementToFocusOnEditRef,
rowAriaLabel = 'row'
}) => {
const editButtonRef = React.useRef<HTMLButtonElement>();

const onKeyDown = (event: React.KeyboardEvent<HTMLButtonElement>, button: 'edit' | 'stopEditing') => {
const focusRef = button === 'edit' ? elementToFocusOnEditRef : editButtonRef;

if (event.key === KeyTypes.Enter || event.key === KeyTypes.Space) {
// because space key triggers click event before keyDown, we have to prevent default behaviour and trigger click manually
event.preventDefault();
(event.target as HTMLButtonElement).click();
setTimeout(() => {
focusRef?.current?.focus();
}, 0);
}
};

return (
<>
<Td
dataLabel="Save and cancel buttons"
className={css(
inlineEditStyles.inlineEditGroup,
inlineEditStyles.modifiers.iconGroup,
inlineEditStyles.modifiers.actionGroup
)}
>
<div className={css(inlineEditStyles.inlineEditAction, inlineEditStyles.modifiers.valid)}>
<Button
aria-label={`Save edits of ${rowAriaLabel}`}
onClick={() => onClick('save')}
onKeyDown={(event) => onKeyDown(event, 'stopEditing')}
variant="plain"
>
<CheckIcon />
</Button>
</div>
<div className={css(inlineEditStyles.inlineEditAction)}>
<Button
aria-label={`Discard edits of ${rowAriaLabel}`}
onClick={() => onClick('cancel')}
onKeyDown={(event) => onKeyDown(event, 'stopEditing')}
variant="plain"
>
<TimesIcon />
</Button>
</div>
</Td>
<Td
dataLabel="Edit button"
className={css(inlineEditStyles.inlineEditAction, inlineEditStyles.modifiers.enableEditable)}
>
<Button
ref={editButtonRef}
aria-label={`Edit ${rowAriaLabel}`}
onClick={() => onClick('edit')}
onKeyDown={(event) => onKeyDown(event, 'edit')}
variant="plain"
>
<PencilAltIcon />
</Button>
</Td>
</>
);
};

interface EditableCellProps {
dataLabel: string;
staticValue: React.ReactNode;
editingValue: React.ReactNode;
role?: string;
ariaLabel?: string;
}

const EditableCell: React.FunctionComponent<EditableCellProps> = ({
dataLabel,
staticValue,
editingValue,
role,
ariaLabel
}) => {
const hasMultipleInputs = Array.isArray(editingValue) && editingValue.every((elem) => React.isValidElement(elem));

return (
<Td dataLabel={dataLabel}>
<div className={css(inlineEditStyles.inlineEditValue)}>{staticValue}</div>
{hasMultipleInputs ? (
<div className={css(inlineEditStyles.inlineEditGroup, 'pf-m-column')} role={role} aria-label={ariaLabel}>
{(editingValue as React.ReactElement[]).map((elem, index) => (
<div key={index} className={css(inlineEditStyles.inlineEditInput)}>
{elem}
</div>
))}
</div>
) : (
<div className={css(inlineEditStyles.inlineEditInput)}>{editingValue}</div>
)}
</Td>
);
};

interface EditableRow {
data: CustomData;
columnNames: ColumnNames<CustomData>;
dataOptions?: CustomDataOptions;
saveChanges: (editedData: CustomData) => void;
ariaLabel: string;
rowIndex?: number;
}

const EditableRow: React.FunctionComponent<EditableRow> = ({
data,
columnNames,
dataOptions,
saveChanges,
ariaLabel,
rowIndex
}) => {
const [editable, setEditable] = React.useState(false);
const [editedData, setEditedData] = React.useState(data);

const inputRef = React.useRef();

return (
<Tr className={css(inlineEditStyles.inlineEdit, editable ? inlineEditStyles.modifiers.inlineEditable : '')}>
<EditableCell
dataLabel={columnNames.textInput}
staticValue={data.textInput}
editingValue={
<TextInput
aria-label={`${columnNames.textInput} ${ariaLabel}`}
ref={inputRef}
value={editedData.textInput}
onChange={(e) => setEditedData((data) => ({ ...data, textInput: (e.target as HTMLInputElement).value }))}
/>
}
/>
<EditableCell
dataLabel={columnNames.textInputDisabled}
staticValue={data.textInputDisabled}
editingValue={
<TextInput
aria-label={`${columnNames.textInputDisabled} ${ariaLabel}`}
isDisabled={true}
value={editedData.textInputDisabled ?? ''}
/>
}
/>
<EditableCell
dataLabel={columnNames.checkboxes}
staticValue={data.checkboxes.join(', ')}
role="group"
ariaLabel={`Checkbox group row ${rowIndex}`}
editingValue={dataOptions?.checkboxes.map((option) => {
const id = getUniqueId('checkbox');
return (
<Checkbox
key={id}
name={id}
id={id}
label={option}
isChecked={editedData.checkboxes.includes(option)}
onChange={(_e, checked) =>
setEditedData((data) => ({
...data,
checkboxes: checked ? [...data.checkboxes, option] : data.checkboxes.filter((item) => item !== option)
}))
}
/>
);
})}
/>
<EditableCell
dataLabel={columnNames.radios}
staticValue={data.radios}
ariaLabel={`Radio button group row ${rowIndex}`}
role="radiogroup"
editingValue={dataOptions?.radios.map((option) => {
const id = getUniqueId('radio');
return (
<Radio
key={id}
name={id}
id={id}
label={option}
isChecked={editedData.radios === option}
onChange={() => setEditedData((data) => ({ ...data, radios: option }))}
/>
);
})}
/>
<EditButtonsCell
onClick={(type) => {
type === 'edit' ? setEditable(true) : setEditable(false);
type === 'save' && saveChanges(editedData);
type === 'cancel' && setEditedData(data);
}}
rowAriaLabel={ariaLabel}
elementToFocusOnEditRef={inputRef}
/>
</Tr>
);
};

interface CustomData {
textInput: string;
textInputDisabled: string | null;
checkboxes: string[];
radios: string;
}

interface CustomDataOptions {
checkboxes: string[];
radios: string[];
}

type ColumnNames<T> = { [K in keyof T]: string };

export const TableEditable: React.FunctionComponent = () => {
// In real usage, this data would come from some external source like an API via props.
const initialRows: CustomData[] = [
{
textInput: 'Editable text 1',
textInputDisabled: 'Non-editable text 1',
checkboxes: ['Option A'],
radios: 'Option A'
},
{
textInput: 'Editable text 2',
textInputDisabled: null,
checkboxes: [],
radios: 'Option B'
},
{
textInput: 'Editable text 3',
textInputDisabled: 'Non-editable text 3',
checkboxes: ['Option A', 'Option B'],
radios: 'Option A'
}
];

// List of all selectable options for some cells of initialRows
const initialRowsOptions: CustomDataOptions[] = [
{
checkboxes: ['Option A', 'Option B', 'Option C'],
radios: ['Option A', 'Option B']
},
{
checkboxes: ['Option A', 'Option B'],
radios: ['Option A', 'Option B', 'Option C']
},
{
checkboxes: ['Option A', 'Option B'],
radios: ['Option A', 'Option B']
}
];

const [rows, setRows] = React.useState(initialRows);

const columnNames: ColumnNames<CustomData> = {
textInput: 'Text input',
textInputDisabled: 'Disabled text input',
checkboxes: 'Checkboxes',
radios: 'Radios'
};

return (
<Table aria-label="Editable table">
<Thead>
<Tr>
<Th>{columnNames.textInput}</Th>
<Th>{columnNames.textInputDisabled}</Th>
<Th>{columnNames.checkboxes}</Th>
<Th>{columnNames.radios}</Th>
<Th screenReaderText="Row edit actions" />
</Tr>
</Thead>
<Tbody>
{rows.map((data, index) => (
<EditableRow
key={index}
data={data}
rowIndex={index}
dataOptions={initialRowsOptions[index]}
columnNames={columnNames}
saveChanges={(editedRow) => {
setRows((rows) => rows.map((row, i) => (i === index ? editedRow : row)));
}}
ariaLabel={`row ${index + 1}`}
></EditableRow>
))}
</Tbody>
</Table>
);
};

0 comments on commit 1779fcd

Please sign in to comment.