Skip to content

[WIP]: Explore editmode #8628

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

Draft
wants to merge 5 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
117 changes: 113 additions & 4 deletions packages/@react-aria/grid/src/useGridCell.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,13 @@
*/

import {DOMAttributes, FocusableElement, Key, RefObject} from '@react-types/shared';
import {focusSafely, isFocusVisible} from '@react-aria/interactions';
import {focusSafely, isFocusVisible, useLongPress} from '@react-aria/interactions';
import {getFocusableTreeWalker} from '@react-aria/focus';
import {getScrollParent, mergeProps, scrollIntoViewport} from '@react-aria/utils';
import {GridCollection, GridNode} from '@react-types/grid';
import {gridMap} from './utils';
import {GridState} from '@react-stately/grid';
import {KeyboardEvent as ReactKeyboardEvent, useRef} from 'react';
import {KeyboardEvent as ReactKeyboardEvent, useEffect, useRef} from 'react';
import {useLocale} from '@react-aria/i18n';
import {useSelectableItem} from '@react-aria/selection';

Expand All @@ -44,7 +44,9 @@ export interface GridCellAria {
/** Props for the grid cell element. */
gridCellProps: DOMAttributes,
/** Whether the cell is currently in a pressed state. */
isPressed: boolean
isPressed: boolean,
/** Whether the cell is currently in edit mode. */
isEditing: boolean
}

/**
Expand Down Expand Up @@ -280,9 +282,116 @@ export function useGridCell<T, C extends GridCollection<T>>(props: GridCellProps
};
}


// Edit mode.
let isEditing = state.editingKey === node.key;
let modality = useRef('');
let isDoubleClickAllowed = /* state.selectionManager.isEmpty || */ !state.selectionManager.canSelectItem(node.parentKey!) || state.selectionManager.isSelected(node.parentKey!);
let editModeProps = {
onDoubleClick(e) {
// TODO: this conflicts with double click to perform an action with selectionBehavior="replace".
// With selectionBehavior="toggle", the action will also fire for each click. Probably should delay to ensure it wasn't a double click?
// If there is no action, then the selection will also toggle twice. Maybe you have to select via checkbox if there is editing?

if (modality.current === 'touch') {
return;
}

// With selectionBehavior="toggle", only support double click to edit when nothing is selected. See below.
// if (state.selectionManager.selectionBehavior !== 'toggle' || isDoubleClickAllowed) {
if (isDoubleClickAllowed) {
e.stopPropagation();
state.startEditing(node.key);
}
},
onPointerDown(e) {
// If selectionBehavior="toggle", we need to prevent row selection from toggling when trying to double click to edit.
// When no rows are selected, users can double click a cell to edit, or click the checkbox to start selecting.
// Once at least one row is selected, clicking other rows will select them, and double clicking does nothing.
modality.current = e.pointerType;
if (
state.selectionManager.selectionBehavior === 'toggle' &&
e.pointerType !== 'touch' &&
isDoubleClickAllowed
) {
// TODO: this doesn't work on touch because touch with selectionBehavior="replace" works like toggle.
// But if we prevent this, there is no other way to initiate a selection (e.g. checkbox).
// We could add long press, but seems like
e.stopPropagation();
// focus();
// state.selectionManager.setFocused(true);
// state.selectionManager.setFocusedKey(node.parentKey);
}

if (isEditing) {
e.stopPropagation();
}
},
onMouseDown(e) {
if (isEditing) {
e.stopPropagation();
}
},
onKeyDown(e) {
if (e.key === 'Escape' && isEditing) {
e.stopPropagation();
state.endEditing();
} else if (e.key === 'Enter') {
e.stopPropagation();
if (e.ctrlKey || e.metaKey || e.altKey || e.shiftKey) {
return;
}
if (!isEditing) {
state.startEditing(node.key);
} else {
// Maybe not by default. User will want to save the value first.
// state.endEditing();
}
} else if (!isEditing && (e.key.length === 1 || !/^[A-Z]/i.test(e.key))) {
// TODO: somehow only if the edit mode is a text input?
e.stopPropagation();
state.startEditing(node.key);
} else if (isEditing) {
e.stopPropagation();
}
},
onBlur(e) {
if (isEditing && !e.currentTarget.contains(e.relatedTarget)) {
state.endEditing();
}
}
};

let {longPressProps} = useLongPress({
isDisabled: isEditing,
onLongPress(e) {
if (e.pointerType === 'touch') {
state.startEditing(node.key);
}
}
});

if (node.props?.allowsEditing) {
gridCellProps = isEditing ? {...gridCellProps, ...editModeProps} : mergeProps(gridCellProps, longPressProps, editModeProps);
}

let wasEditing = useRef(false);
useEffect(() => {
if (ref.current && isEditing !== wasEditing.current && (isEditing || (state.selectionManager.isFocused && state.selectionManager.focusedKey === node.key))) {
focus();
let input = document.activeElement;
if ((input instanceof HTMLInputElement || input instanceof HTMLTextAreaElement) && ref.current.contains(input)) {
input.select();
}
}

wasEditing.current = isEditing;
}, [isEditing, state.selectionManager.isFocused, state.selectionManager.focusedKey, focus, node.key, ref]);

return {
gridCellProps,
isPressed
isPressed,
isEditing
};
}

Expand Down
9 changes: 6 additions & 3 deletions packages/@react-aria/table/src/useTableCell.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,9 @@ export interface TableCellAria {
/** Props for the table cell element. */
gridCellProps: DOMAttributes,
/** Whether the cell is currently in a pressed state. */
isPressed: boolean
isPressed: boolean,
/** Whether the cell is currently in edit mode. */
isEditing: boolean
}

/**
Expand All @@ -45,7 +47,7 @@ export interface TableCellAria {
* @param ref - The ref attached to the cell element.
*/
export function useTableCell<T>(props: AriaTableCellProps, state: TableState<T>, ref: RefObject<FocusableElement | null>): TableCellAria {
let {gridCellProps, isPressed} = useGridCell(props, state, ref);
let {gridCellProps, isPressed, isEditing} = useGridCell(props, state, ref);
let columnKey = props.node.column?.key;
if (columnKey != null && state.collection.rowHeaderColumnKeys.has(columnKey)) {
gridCellProps.role = 'rowheader';
Expand All @@ -54,6 +56,7 @@ export function useTableCell<T>(props: AriaTableCellProps, state: TableState<T>,

return {
gridCellProps,
isPressed
isPressed,
isEditing
};
}
29 changes: 25 additions & 4 deletions packages/@react-stately/grid/src/useGridState.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import {getChildNodes, getFirstItem, getLastItem} from '@react-stately/collectio
import {GridCollection, GridNode} from '@react-types/grid';
import {Key} from '@react-types/shared';
import {MultipleSelectionState, MultipleSelectionStateProps, SelectionManager, useMultipleSelectionState} from '@react-stately/selection';
import {useEffect, useMemo, useRef} from 'react';
import {useEffect, useMemo, useRef, useState} from 'react';

export interface GridState<T, C extends GridCollection<T>> {
collection: C,
Expand All @@ -11,7 +11,15 @@ export interface GridState<T, C extends GridCollection<T>> {
/** A selection manager to read and update row selection state. */
selectionManager: SelectionManager,
/** Whether keyboard navigation is disabled, such as when the arrow keys should be handled by a component within a cell. */
isKeyboardNavigationDisabled: boolean
isKeyboardNavigationDisabled: boolean,
/** Set whether keyboard navigation is disabled, such as when the arrow keys should be handled by a component within a cell. */
setKeyboardNavigationDisabled: (val: boolean) => void,
/** The currently editing cell in the table. */
editingKey: Key | null,
/** Starts editing the given cell. */
startEditing(key: Key): void,
/** Exits edit mode. */
endEditing(): void
}

export interface GridStateOptions<T, C extends GridCollection<T>> extends MultipleSelectionStateProps {
Expand Down Expand Up @@ -113,10 +121,23 @@ export function useGridState<T extends object, C extends GridCollection<T>>(prop
cachedCollection.current = collection;
}, [collection, selectionManager, selectionState, selectionState.focusedKey]);

let [isKeyboardNavigationDisabled, setKeyboardNavigationDisabled] = useState(false);
let [editingKey, setEditingKey] = useState<Key | null>(null);

return {
collection,
disabledKeys,
isKeyboardNavigationDisabled: false,
selectionManager
isKeyboardNavigationDisabled: collection.size === 0 || isKeyboardNavigationDisabled,
setKeyboardNavigationDisabled,
selectionManager,
editingKey,
startEditing(key) {
setEditingKey(key);
setKeyboardNavigationDisabled(true);
},
endEditing() {
setEditingKey(null);
setKeyboardNavigationDisabled(false);
}
};
}
16 changes: 4 additions & 12 deletions packages/@react-stately/table/src/useTableState.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import {GridState, useGridState} from '@react-stately/grid';
import {TableCollection as ITableCollection, TableBodyProps, TableHeaderProps} from '@react-types/table';
import {Key, Node, SelectionMode, Sortable, SortDescriptor, SortDirection} from '@react-types/shared';
import {MultipleSelectionState, MultipleSelectionStateProps} from '@react-stately/selection';
import {ReactElement, useCallback, useMemo, useState} from 'react';
import {ReactElement, useCallback, useMemo} from 'react';
import {TableCollection} from './TableCollection';
import {useCollection} from '@react-stately/collections';

Expand All @@ -26,11 +26,7 @@ export interface TableState<T> extends GridState<T, ITableCollection<T>> {
/** The current sorted column and direction. */
sortDescriptor: SortDescriptor | null,
/** Calls the provided onSortChange handler with the provided column key and sort direction. */
sort(columnKey: Key, direction?: 'ascending' | 'descending'): void,
/** Whether keyboard navigation is disabled, such as when the arrow keys should be handled by a component within a cell. */
isKeyboardNavigationDisabled: boolean,
/** Set whether keyboard navigation is disabled, such as when the arrow keys should be handled by a component within a cell. */
setKeyboardNavigationDisabled: (val: boolean) => void
sort(columnKey: Key, direction?: 'ascending' | 'descending'): void
}

export interface CollectionBuilderContext<T> {
Expand Down Expand Up @@ -67,7 +63,6 @@ const OPPOSITE_SORT_DIRECTION = {
* of columns and rows from props. In addition, it tracks row selection and manages sort order changes.
*/
export function useTableState<T extends object>(props: TableStateProps<T>): TableState<T> {
let [isKeyboardNavigationDisabled, setKeyboardNavigationDisabled] = useState(false);
let {selectionMode = 'none', showSelectionCheckboxes, showDragButtons} = props;

let context = useMemo(() => ({
Expand All @@ -83,20 +78,17 @@ export function useTableState<T extends object>(props: TableStateProps<T>): Tabl
useCallback((nodes) => new TableCollection(nodes, null, context), [context]),
context
);
let {disabledKeys, selectionManager} = useGridState({
let gridState = useGridState({
...props,
collection,
disabledBehavior: props.disabledBehavior || 'selection'
});

return {
...gridState,
collection,
disabledKeys,
selectionManager,
showSelectionCheckboxes: props.showSelectionCheckboxes || false,
sortDescriptor: props.sortDescriptor ?? null,
isKeyboardNavigationDisabled: collection.size === 0 || isKeyboardNavigationDisabled,
setKeyboardNavigationDisabled,
sort(columnKey: Key, direction?: 'ascending' | 'descending') {
props.onSortChange?.({
column: columnKey,
Expand Down
33 changes: 28 additions & 5 deletions packages/react-aria-components/src/Table.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1189,7 +1189,20 @@ export interface CellRenderProps {
/**
* The unique id of the cell.
**/
id?: Key
id?: Key,
/**
* Whether the cell is currently in edit mode.
* @selector [data-editing]
*/
isEditing: boolean,
/**
* Starts editing the cell.
*/
startEditing: () => void,
/**
* Ends editing the cell.
*/
endEditing: () => void
}

export interface CellProps extends RenderProps<CellRenderProps>, GlobalDOMAttributes<HTMLTableCellElement> {
Expand All @@ -1198,7 +1211,9 @@ export interface CellProps extends RenderProps<CellRenderProps>, GlobalDOMAttrib
/** A string representation of the cell's contents, used for features like typeahead. */
textValue?: string,
/** Indicates how many columns the data cell spans. */
colSpan?: number
colSpan?: number,
/** Whether the cell allows inline editing. */
allowsEditing?: boolean
}

/**
Expand All @@ -1212,7 +1227,7 @@ export const Cell = /*#__PURE__*/ createLeafComponent('cell', (props: CellProps,

cell.column = state.collection.columns[cell.index];

let {gridCellProps, isPressed} = useTableCell({
let {gridCellProps, isPressed, isEditing} = useTableCell({
node: cell,
shouldSelectOnPressUp: !!dragState,
isVirtualized
Expand All @@ -1229,7 +1244,14 @@ export const Cell = /*#__PURE__*/ createLeafComponent('cell', (props: CellProps,
isFocusVisible,
isPressed,
isHovered,
id: cell.key
id: cell.key,
isEditing,
startEditing() {
state.startEditing(cell.key);
},
endEditing() {
state.endEditing();
}
}
});

Expand All @@ -1243,7 +1265,8 @@ export const Cell = /*#__PURE__*/ createLeafComponent('cell', (props: CellProps,
ref={ref as any}
data-focused={isFocused || undefined}
data-focus-visible={isFocusVisible || undefined}
data-pressed={isPressed || undefined}>
data-pressed={isPressed || undefined}
data-editing={isEditing || undefined}>
<CollectionRendererContext.Provider value={DefaultCollectionRenderer}>
{renderProps.children}
</CollectionRendererContext.Provider>
Expand Down
Loading