Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Editor2 #2102

Merged
merged 55 commits into from
Aug 17, 2020
Merged

Editor2 #2102

merged 55 commits into from
Aug 17, 2020

Conversation

amanmahajan7
Copy link
Contributor

@amanmahajan7 amanmahajan7 commented Jul 24, 2020

New API

column.editor2?: React.ComponentType<Editor2Props<TRow, TSummaryRow>>;
column.editorOptions?: {
    /** Default: true for editor1 and false for editor2 */
    createPortal?: boolean;
    /** Default: false */
    editOnClick?: boolean;
    /** Prevent default to cancel editing */
    onCellKeyDown?: (event: React.KeyboardEvent<HTMLDivElement>) => void;
};

export interface Editor2Props<TRow, TSummaryRow = any> {
  row: Readonly<TRow>;
  rowIdx: number;
  column: Readonly<CalculatedColumn<TRow, TSummaryRow>>;
  rowHeight: number;
  onRowChange: (row: Readonly<TRow>, commitChanges?: boolean) => void;
  onClose: (commitChanges?: boolean) => void;
  top: number;
  left: number;
  editorPortalTarget: Element;
}

How to use

1) Use `column.editor2`

   {
      key: 'country',
      name: 'Country',
      width: 180,
      editor2: p => (
        <SelectEditor
          value={p.row.country}
          onChange={value => p.onRowChange({ ...p.row, country: value })}
          options={countries.map(c => ({ value: c, label: c }))}
          rowHeight={p.rowHeight}
          menuPortalTarget={p.editorPortalTarget}
        />
      ),
      resizable: true,
      sortable: true
    },
    {
      key: 'contact',
      name: 'Contact',
      width: 160,
      editor2: p => (
        <TextEditor
          value={p.row.contact}
          onChange={value => p.onRowChange({ ...p.row, contact: value })}
          rowHeight={p.rowHeight}
        />
      ),
      resizable: true,
      sortable: true
    }

2) Handle `onRowsChange`

onRowsChange={updatedRows => setRows(updatedRows )}

DataGrid now keeps the updated row in a local state so it is easy to handle commit, cancel and validations (future).

@amanmahajan7 amanmahajan7 changed the title Add unsafe_editorProps Editor2 Jul 27, 2020
@amanmahajan7 amanmahajan7 mentioned this pull request Jul 27, 2020
3 tasks
@amanmahajan7 amanmahajan7 marked this pull request as ready for review July 27, 2020 22:23
src/DataGrid.tsx Outdated
if (isUsingEditor2) {
if (key === 'Delete' || key === 'Backspace') {
setEditorValue('');
} else if (key === 'Enter' || key === 'F2') {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@@ -504,8 +548,10 @@ function DataGrid<R, K extends keyof R, SR>({

function selectCell(position: Position, enableEditor = false): void {
if (!isCellWithinBounds(position)) return;
handleCommit2();
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it is better to have some control over when the value is committed. This way we can add validation in the future. Using an effect to commit the value is probably not the best way
https://github.com/adazzle/react-data-grid/blob/c/src/editors/EditorContainer.tsx#L70

src/DataGrid.tsx Outdated

if (canOpenEditor && (key === 'Enter' || isActivatedByUser)) {
const isActivatedByUser = isUsingEditor2
? defaultCellInput(event) && column.editor2Props?.onCellInput?.(event) !== false
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should open the editor on Enter, F2, or on any other alpha numeric key. If the users need to restrict it then they can use onCellInput . This is also the recommended way
https://www.w3.org/TR/wai-aria-practices/#gridNav_inside

src/types.ts Outdated
editor2Props?: {
createPortal?: boolean;
editOnSingleClick?: boolean;
commitOnOutsideClick?: boolean;
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This needs to be implemented.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perhaps add a TODO comment?

}
// editor2Props: {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we can handle it differently using https://www.w3.org/TR/wai-aria-practices/#gridNav_focus

export function TextEditor<R>({ value, onChange, rowHeight }: Editor2Props<string, R>) {
const inputRef = useRef<HTMLInputElement>(null);

useLayoutEffect(() => {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We no longer have the bug where the input value is initially selected and typing another key deletes it

})
}}
onKeyDown={event => {
if (['ArrowDown', 'ArrowUp', 'Enter'].includes(event.key) && selectRef.current!.getProp('menuIsOpen')) {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is the best way to cancel keyboard navigation IMO

@amanmahajan7 amanmahajan7 self-assigned this Jul 28, 2020
src/DataGrid.tsx Outdated

if (canOpenEditor && (key === 'Enter' || isActivatedByUser)) {
if (selectedPosition.mode === 'EDIT') {
if (isUsingEditor2 && (key === 'Enter' || key === 'F2')) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What happens if we have such editors that has 2 or more input / textarea?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it is not possible to have one solution for every type of editor and we need some way to to tweak the default behavior. If the editor do not need to commit changes on Enter then it should handle the keydown event and stop propagation. The question is what default behavior we want to use? Right now we do the following

  • Open editor on Enter, F2 and alphanumeric keys
  • Cancel changes on Escape
  • Commit changes on Enter and F2
  • Commit changes when another cell is clicked
  • Commit changes when header or summary row is clicked

Any suggestions?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think for the Enter case, we shall leave a prop for the user to decide if we want to use Enter to close the the editor.
It is a common use case that people might want to put a save button with 2 inputs for example, Enter would mean something else for them. As for F2, maybe we should keep like excel to only open the cell rather than closing it. Esc is pretty straightforward, just close without saving / commit.

src/Cell.tsx Outdated
rowIdx={rowIdx}
row={row}
column={column}
left={gridLeft}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we move left / top before ln#80, so that we can retain the ability for overriding the position to make it more flexible?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Editor can choose not to use left, right props. The only way to override them is in the editor itself. We control editor2Props and not the user

src/DataGrid.tsx Outdated Show resolved Hide resolved
src/types.ts Outdated
editor2Props?: {
createPortal?: boolean;
editOnSingleClick?: boolean;
commitOnOutsideClick?: boolean;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perhaps add a TODO comment?

src/utils/keyboardUtils.ts Outdated Show resolved Hide resolved
src/DataGrid.tsx Outdated
Comment on lines 400 to 407
onRowsUpdate?.({
cellKey,
fromRow: rowIdx,
toRow: rowIdx,
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
updated: { [cellKey]: editorValue } as never,
action: UpdateActions.CELL_UPDATE
});
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd like to rethink this someday, maybe something like

// single value update
onDataUpdate({
  type: ENUM.UPDATE_TYPE,
  value: unknown, // is it possible to type this? probably not, I think each column should have its own handler
  row,
  rowIdx,
  column
})

// multi value 
update
onDataUpdate({
  type: ENUM.MULTI_UPDATE_TYPE,
  updates: [{ value, row, rowIdx, column }]
})

An object for updated doesn't make sense to me, going forward we should aim to make rows completely generic.

src/DataGrid.tsx Outdated
@@ -223,6 +223,7 @@ function DataGrid<R, K extends keyof R, SR>({
const [copiedPosition, setCopiedPosition] = useState<Position & { value: unknown } | null>(null);
const [isDragging, setDragging] = useState(false);
const [draggedOverRowIdx, setOverRowIdx] = useState<number | undefined>(undefined);
const [editorValue, setEditorValue] = useState<unknown>();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this state and all the handlers really be in the root component?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We need to control when the value gets committed #2102 (comment) and for that root component needs access to editorValue. Any other way to handle it?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I thought maybe the editor container could have it in its scope

src/DataGrid.tsx Outdated
if (isCellEditable(selectedPosition) && defaultCellInput(event)) {
if (isUsingEditor2) {
if (key === 'Delete' || key === 'Backspace') {
setEditorValue('');
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

'' doesn't make sense for non-string columns.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we need to define the default behavior and provide a way to override it
#2102 (comment)

src/DataGrid.tsx Outdated Show resolved Hide resolved
src/DataGrid.tsx Outdated
if (key === 'Delete' || key === 'Backspace') {
setEditorValue('');
} else if (key === 'Enter' || key === 'F2') {
setEditorValue(column.editor2Options?.getInitialValue ? column.editor2Options?.getInitialValue(event, row) : row[column.key as keyof R]);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

row[column.key as keyof R] assumes that the row is an object.

@@ -133,7 +133,7 @@ export default function EditorContainer<R, SR>({
function onKeyDown(e: KeyboardEvent) {
if (preventDefaultNavigation(e.key)) {
e.stopPropagation();
} else if (['Enter', 'Tab', 'ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight'].includes(e.key)) {
} else if (['Enter', 'F2', 'Tab', 'ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight'].includes(e.key)) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In Windows' file explorer, if I press F2 twice, it doesn't commit anything, it keeps the editing state.
Same thing in a google spreadsheet.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can remove it. I was following the accessibility guidelines

src/types.ts Outdated
Comment on lines 43 to 44
getInitialValue?: (event: React.KeyboardEvent<HTMLDivElement>, row: TRow) => unknown;
onCellInput?: (event: React.KeyboardEvent<HTMLDivElement>) => void;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's the difference between these two?

Co-authored-by: Nicolas Stepien <567105+nstepien@users.noreply.github.com>
@adazzle adazzle deleted a comment from nstepien Jul 30, 2020
@@ -29,6 +32,7 @@ function SummaryRow<R, SR>({
aria-rowindex={ariaRowIndex}
className={`rdg-row rdg-row-${rowIdx % 2 === 0 ? 'even' : 'odd'} rdg-summary-row`}
style={{ bottom }}
onClick={onClick}
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I accidentally deleted your comment

Why this change? I don't think we expose SummaryRow?

So we can commit the value when summary row is clicked. We need to decide on when to commit the values

Copy link
Contributor

@nstepien nstepien left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Needs a changelog

src/DataGrid.tsx Outdated Show resolved Hide resolved
src/DataGrid.tsx Outdated Show resolved Hide resolved
src/DataGrid.tsx Outdated Show resolved Hide resolved
src/types.ts Outdated Show resolved Hide resolved
src/types.ts Outdated
editor2Options?: {
createPortal?: boolean;
singleClickEdit?: boolean;
commitOnOutsideClick?: boolean;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's remove or comment this out if it won't be implemented in this PR.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It was a quick implementation so I added it

src/types.ts Outdated
// editOnDoubleClick?: boolean;
// commitOnScroll?: boolean;
// commitOnOutsideClick?: boolean;
onCellInput?: (event: React.KeyboardEvent<HTMLDivElement>) => void; // Prevent default to cancel editing
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we rename it to onKeyDown or onCellKeyDown?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's only used when the editor is closed right? Then onCellKeyDown makes more sense.

src/DataGrid.tsx Outdated
}

function handleSort(columnKey: string, direction: SortDirection) {
handleCommit2();
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is still needed as the cell is unmounted before useOutsideClick runs

))}
{viewportColumns.map(column => {
const isCellSelected = selectedCellProps?.idx === column.idx;
if (selectedCellProps?.mode === 'EDIT' && isCellSelected) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
if (selectedCellProps?.mode === 'EDIT' && isCellSelected) {
if (isCellSelected && selectedCellProps!.mode === 'EDIT') {

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

image

src/Row.tsx Outdated
isCellSelected={isCellSelected}
isRowSelected={isRowSelected}
eventBus={eventBus}
dragHandleProps={isCellSelected ? (selectedCellProps as SelectedCellProps)?.dragHandleProps : undefined}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need the type assertion and ?.?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, TS does not recognize it

image

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if isCellSelected is true, then we can assert that selectedCellProps isn't nullish, so we can add !.

src/Row.tsx Outdated
isRowSelected={isRowSelected}
eventBus={eventBus}
dragHandleProps={isCellSelected ? (selectedCellProps as SelectedCellProps)?.dragHandleProps : undefined}
onKeyDown={isCellSelected ? selectedCellProps?.onKeyDown : undefined}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need ?.?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same reason.

@amanmahajan7 amanmahajan7 merged commit ad19cc4 into canary Aug 17, 2020
@amanmahajan7 amanmahajan7 deleted the am-editorprops branch August 17, 2020 12:50
This was referenced Aug 19, 2020
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

3 participants