From acd45ff2bcd0b1e6c3e955604cb7471a2de49c99 Mon Sep 17 00:00:00 2001 From: adamviktora Date: Wed, 24 Apr 2024 15:38:37 +0200 Subject: [PATCH 1/6] docs(Table): add Editable Table example --- .../src/components/Table/examples/Table.md | 11 + .../Table/examples/TableEditable.tsx | 239 ++++++++++++++++++ 2 files changed, 250 insertions(+) create mode 100644 packages/react-table/src/components/Table/examples/TableEditable.tsx 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..9beef201160 --- /dev/null +++ b/packages/react-table/src/components/Table/examples/TableEditable.tsx @@ -0,0 +1,239 @@ +import React from 'react'; +import { Table, Thead, Tr, Th, Tbody, Td } from '@patternfly/react-table'; +import { Button } from '@patternfly/react-core/dist/esm/components/Button'; +import { Checkbox } from '@patternfly/react-core/dist/esm/components/Checkbox'; +import { Radio } from '@patternfly/react-core/dist/esm/components/Radio'; +import { TextInput } from '@patternfly/react-core/dist/esm/components/TextInput'; +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'; +import { getUniqueId } from '@patternfly/react-core/dist/esm/helpers'; + +interface EditColumnProps { + onClick: (type: 'save' | 'cancel' | 'edit') => void; + saveAriaLabel?: string; + cancelAriaLabel?: string; + editAriaLabel?: string; +} + +const EditColumn: React.FunctionComponent = ({ + onClick, + saveAriaLabel = 'Save edits', + cancelAriaLabel = 'Cancel edits', + editAriaLabel = 'Edit', + ...props +}: EditColumnProps) => ( + +
+
+ +
+
+ +
+
+
+ +
+
+); + +interface EditableCellProps { + dataLabel: string; + staticValue: React.ReactNode; + editingValue: React.ReactNode; +} + +const EditableCell = ({ dataLabel, staticValue, editingValue }: EditableCellProps) => { + 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; +} + +const EditableRow = ({ data, columnNames, dataOptions, saveChanges }: EditableRow) => { + const [editable, setEditable] = React.useState(false); + const [editedData, setEditedData] = React.useState(data); + + 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); + }} + /> + + ); +}; + +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))); + }} + > + ))} + +
{columnNames.textInput}{columnNames.textInputDisabled}{columnNames.checkboxes}{columnNames.radios}
+ ); +}; From df058203e4c1c8dbfce4849575ada99f1c3986ee Mon Sep 17 00:00:00 2001 From: adamviktora Date: Mon, 13 May 2024 09:52:23 +0200 Subject: [PATCH 2/6] fix(TableEditable example): group imports --- .../src/components/Table/examples/TableEditable.tsx | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/packages/react-table/src/components/Table/examples/TableEditable.tsx b/packages/react-table/src/components/Table/examples/TableEditable.tsx index 9beef201160..0e3da27fd49 100644 --- a/packages/react-table/src/components/Table/examples/TableEditable.tsx +++ b/packages/react-table/src/components/Table/examples/TableEditable.tsx @@ -1,15 +1,11 @@ import React from 'react'; import { Table, Thead, Tr, Th, Tbody, Td } from '@patternfly/react-table'; -import { Button } from '@patternfly/react-core/dist/esm/components/Button'; -import { Checkbox } from '@patternfly/react-core/dist/esm/components/Checkbox'; -import { Radio } from '@patternfly/react-core/dist/esm/components/Radio'; -import { TextInput } from '@patternfly/react-core/dist/esm/components/TextInput'; +import { Button, Checkbox, Radio, TextInput, 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'; -import { getUniqueId } from '@patternfly/react-core/dist/esm/helpers'; interface EditColumnProps { onClick: (type: 'save' | 'cancel' | 'edit') => void; From e54e3aa648ba83aeccfeb56948eba92fccd17dc2 Mon Sep 17 00:00:00 2001 From: adamviktora Date: Wed, 22 May 2024 10:51:34 +0200 Subject: [PATCH 3/6] fix(Table editable example): focus when using keyboard --- .../Table/examples/TableEditable.tsx | 84 ++++++++++++++----- 1 file changed, 61 insertions(+), 23 deletions(-) diff --git a/packages/react-table/src/components/Table/examples/TableEditable.tsx b/packages/react-table/src/components/Table/examples/TableEditable.tsx index 0e3da27fd49..1475a4796d3 100644 --- a/packages/react-table/src/components/Table/examples/TableEditable.tsx +++ b/packages/react-table/src/components/Table/examples/TableEditable.tsx @@ -1,6 +1,6 @@ import React from 'react'; import { Table, Thead, Tr, Th, Tbody, Td } from '@patternfly/react-table'; -import { Button, Checkbox, Radio, TextInput, getUniqueId } from '@patternfly/react-core'; +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'; @@ -9,6 +9,7 @@ import { css } from '@patternfly/react-styles'; interface EditColumnProps { onClick: (type: 'save' | 'cancel' | 'edit') => void; + elementToFocusOnEditRef?: React.MutableRefObject; saveAriaLabel?: string; cancelAriaLabel?: string; editAriaLabel?: string; @@ -16,31 +17,64 @@ interface EditColumnProps { const EditColumn: React.FunctionComponent = ({ onClick, + elementToFocusOnEditRef, saveAriaLabel = 'Save edits', cancelAriaLabel = 'Cancel edits', - editAriaLabel = 'Edit', - ...props -}: EditColumnProps) => ( - -
-
- + editAriaLabel = 'Edit' +}) => { + 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; @@ -48,7 +82,7 @@ interface EditableCellProps { editingValue: React.ReactNode; } -const EditableCell = ({ dataLabel, staticValue, editingValue }: EditableCellProps) => { +const EditableCell: React.FunctionComponent = ({ dataLabel, staticValue, editingValue }) => { const hasMultipleInputs = Array.isArray(editingValue) && editingValue.every((elem) => React.isValidElement(elem)); return ( @@ -74,10 +108,12 @@ interface EditableRow { saveChanges: (editedData: CustomData) => void; } -const EditableRow = ({ data, columnNames, dataOptions, saveChanges }: EditableRow) => { +const EditableRow: React.FunctionComponent = ({ data, columnNames, dataOptions, saveChanges }) => { 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 }))} /> @@ -140,6 +177,7 @@ const EditableRow = ({ data, columnNames, dataOptions, saveChanges }: EditableRo type === 'save' && saveChanges(editedData); type === 'cancel' && setEditedData(data); }} + elementToFocusOnEditRef={inputRef} /> ); From 0f1357c8e8a3fa4352552a7169f152cb66c67bc1 Mon Sep 17 00:00:00 2001 From: adamviktora Date: Tue, 11 Jun 2024 12:31:14 +0200 Subject: [PATCH 4/6] fix(TableEditable example): PR review updates --- .../Table/examples/TableEditable.tsx | 59 +++++++++++++------ 1 file changed, 41 insertions(+), 18 deletions(-) diff --git a/packages/react-table/src/components/Table/examples/TableEditable.tsx b/packages/react-table/src/components/Table/examples/TableEditable.tsx index 1475a4796d3..c131f0d198a 100644 --- a/packages/react-table/src/components/Table/examples/TableEditable.tsx +++ b/packages/react-table/src/components/Table/examples/TableEditable.tsx @@ -7,20 +7,16 @@ 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 EditColumnProps { +interface EditButtonsCellProps { onClick: (type: 'save' | 'cancel' | 'edit') => void; elementToFocusOnEditRef?: React.MutableRefObject; - saveAriaLabel?: string; - cancelAriaLabel?: string; - editAriaLabel?: string; + rowAriaLabel: string; } -const EditColumn: React.FunctionComponent = ({ +const EditButtonsCell: React.FunctionComponent = ({ onClick, elementToFocusOnEditRef, - saveAriaLabel = 'Save edits', - cancelAriaLabel = 'Cancel edits', - editAriaLabel = 'Edit' + rowAriaLabel = 'row' }) => { const editButtonRef = React.useRef(); @@ -39,10 +35,17 @@ const EditColumn: React.FunctionComponent = ({ return ( <> -
+
-
-
+ + -
+ ); }; @@ -106,9 +112,16 @@ interface EditableRow { columnNames: ColumnNames; dataOptions?: CustomDataOptions; saveChanges: (editedData: CustomData) => void; + ariaLabel: string; } -const EditableRow: React.FunctionComponent = ({ data, columnNames, dataOptions, saveChanges }) => { +const EditableRow: React.FunctionComponent = ({ + data, + columnNames, + dataOptions, + saveChanges, + ariaLabel +}) => { const [editable, setEditable] = React.useState(false); const [editedData, setEditedData] = React.useState(data); @@ -121,6 +134,7 @@ const EditableRow: React.FunctionComponent = ({ data, columnNames, staticValue={data.textInput} editingValue={ setEditedData((data) => ({ ...data, textInput: (e.target as HTMLInputElement).value }))} @@ -130,7 +144,13 @@ const EditableRow: React.FunctionComponent = ({ data, columnNames, } + editingValue={ + + } /> = ({ data, columnNames, ); })} /> - { type === 'edit' ? setEditable(true) : setEditable(false); type === 'save' && saveChanges(editedData); type === 'cancel' && setEditedData(data); }} + rowAriaLabel={ariaLabel} elementToFocusOnEditRef={inputRef} /> @@ -253,6 +274,7 @@ export const TableEditable: React.FunctionComponent = () => { {columnNames.textInputDisabled} {columnNames.checkboxes} {columnNames.radios} + @@ -265,6 +287,7 @@ export const TableEditable: React.FunctionComponent = () => { saveChanges={(editedRow) => { setRows((rows) => rows.map((row, i) => (i === index ? editedRow : row))); }} + ariaLabel={`row ${index + 1}`} > ))} From 07c7f7fac791f740c7cddc85a67968fc49957a33 Mon Sep 17 00:00:00 2001 From: nicolethoen Date: Tue, 16 Jul 2024 16:06:56 -0400 Subject: [PATCH 5/6] fix: update aria-label, missing wrapper classes --- .../Table/examples/TableEditable.tsx | 25 +++++++++++++------ 1 file changed, 18 insertions(+), 7 deletions(-) diff --git a/packages/react-table/src/components/Table/examples/TableEditable.tsx b/packages/react-table/src/components/Table/examples/TableEditable.tsx index c131f0d198a..8a0beca9ae4 100644 --- a/packages/react-table/src/components/Table/examples/TableEditable.tsx +++ b/packages/react-table/src/components/Table/examples/TableEditable.tsx @@ -86,20 +86,24 @@ interface EditableCellProps { dataLabel: string; staticValue: React.ReactNode; editingValue: React.ReactNode; + role?: string; + ariaLabel?: string; } -const EditableCell: React.FunctionComponent = ({ dataLabel, staticValue, editingValue }) => { +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 as React.ReactElement[]).map((elem, index) => ( +
+ {elem} +
+ ))} +
) : (
{editingValue}
)} @@ -113,6 +117,7 @@ interface EditableRow { dataOptions?: CustomDataOptions; saveChanges: (editedData: CustomData) => void; ariaLabel: string; + rowIndex?: number; } const EditableRow: React.FunctionComponent = ({ @@ -120,7 +125,8 @@ const EditableRow: React.FunctionComponent = ({ columnNames, dataOptions, saveChanges, - ariaLabel + ariaLabel, + rowIndex }) => { const [editable, setEditable] = React.useState(false); const [editedData, setEditedData] = React.useState(data); @@ -155,6 +161,8 @@ const EditableRow: React.FunctionComponent = ({ { const id = getUniqueId('checkbox'); return ( @@ -177,6 +185,8 @@ const EditableRow: React.FunctionComponent = ({ { const id = getUniqueId('radio'); return ( @@ -282,6 +292,7 @@ export const TableEditable: React.FunctionComponent = () => { { From 2d8eb8cf6bc61d7c8904abb35e2b1b63152aff2c Mon Sep 17 00:00:00 2001 From: nicolethoen Date: Tue, 16 Jul 2024 16:28:46 -0400 Subject: [PATCH 6/6] fix lint errors --- .../src/components/Table/examples/TableEditable.tsx | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/packages/react-table/src/components/Table/examples/TableEditable.tsx b/packages/react-table/src/components/Table/examples/TableEditable.tsx index 8a0beca9ae4..e64a50fa880 100644 --- a/packages/react-table/src/components/Table/examples/TableEditable.tsx +++ b/packages/react-table/src/components/Table/examples/TableEditable.tsx @@ -90,7 +90,13 @@ interface EditableCellProps { ariaLabel?: string; } -const EditableCell: React.FunctionComponent = ({ dataLabel, staticValue, editingValue, role, ariaLabel }) => { +const EditableCell: React.FunctionComponent = ({ + dataLabel, + staticValue, + editingValue, + role, + ariaLabel +}) => { const hasMultipleInputs = Array.isArray(editingValue) && editingValue.every((elem) => React.isValidElement(elem)); return (