diff --git a/packages/react-table/src/components/Table/examples/Table.md b/packages/react-table/src/components/Table/examples/Table.md index a960bbc1efa..1fbbf3fdd56 100644 --- a/packages/react-table/src/components/Table/examples/Table.md +++ b/packages/react-table/src/components/Table/examples/Table.md @@ -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 @@ -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. diff --git a/packages/react-table/src/components/Table/examples/TableEditable.tsx b/packages/react-table/src/components/Table/examples/TableEditable.tsx new file mode 100644 index 00000000000..e64a50fa880 --- /dev/null +++ b/packages/react-table/src/components/Table/examples/TableEditable.tsx @@ -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; + rowAriaLabel: string; +} + +const EditButtonsCell: React.FunctionComponent = ({ + onClick, + elementToFocusOnEditRef, + rowAriaLabel = 'row' +}) => { + const editButtonRef = React.useRef(); + + const onKeyDown = (event: React.KeyboardEvent, 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 ( + <> + +
+ +
+
+ +
+ + + + + + ); +}; + +interface EditableCellProps { + dataLabel: string; + staticValue: React.ReactNode; + editingValue: React.ReactNode; + role?: string; + ariaLabel?: string; +} + +const EditableCell: React.FunctionComponent = ({ + dataLabel, + staticValue, + editingValue, + role, + ariaLabel +}) => { + const hasMultipleInputs = Array.isArray(editingValue) && editingValue.every((elem) => React.isValidElement(elem)); + + return ( + +
{staticValue}
+ {hasMultipleInputs ? ( +
+ {(editingValue as React.ReactElement[]).map((elem, index) => ( +
+ {elem} +
+ ))} +
+ ) : ( +
{editingValue}
+ )} + + ); +}; + +interface EditableRow { + data: CustomData; + columnNames: ColumnNames; + dataOptions?: CustomDataOptions; + saveChanges: (editedData: CustomData) => void; + ariaLabel: string; + rowIndex?: number; +} + +const EditableRow: React.FunctionComponent = ({ + data, + columnNames, + dataOptions, + saveChanges, + ariaLabel, + rowIndex +}) => { + const [editable, setEditable] = React.useState(false); + const [editedData, setEditedData] = React.useState(data); + + const inputRef = React.useRef(); + + return ( + + setEditedData((data) => ({ ...data, textInput: (e.target as HTMLInputElement).value }))} + /> + } + /> + + } + /> + { + const id = getUniqueId('checkbox'); + return ( + + setEditedData((data) => ({ + ...data, + checkboxes: checked ? [...data.checkboxes, option] : data.checkboxes.filter((item) => item !== option) + })) + } + /> + ); + })} + /> + { + const id = getUniqueId('radio'); + return ( + setEditedData((data) => ({ ...data, radios: option }))} + /> + ); + })} + /> + { + type === 'edit' ? setEditable(true) : setEditable(false); + type === 'save' && saveChanges(editedData); + type === 'cancel' && setEditedData(data); + }} + rowAriaLabel={ariaLabel} + elementToFocusOnEditRef={inputRef} + /> + + ); +}; + +interface CustomData { + textInput: string; + textInputDisabled: string | null; + checkboxes: string[]; + radios: string; +} + +interface CustomDataOptions { + checkboxes: string[]; + radios: string[]; +} + +type ColumnNames = { [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 = { + textInput: 'Text input', + textInputDisabled: 'Disabled text input', + checkboxes: 'Checkboxes', + radios: 'Radios' + }; + + return ( + + + + + + + + + + + {rows.map((data, index) => ( + { + setRows((rows) => rows.map((row, i) => (i === index ? editedRow : row))); + }} + ariaLabel={`row ${index + 1}`} + > + ))} + +
{columnNames.textInput}{columnNames.textInputDisabled}{columnNames.checkboxes}{columnNames.radios} +
+ ); +};