Skip to content

Commit

Permalink
[TreeView] Clean label editing code
Browse files Browse the repository at this point in the history
  • Loading branch information
flaviendelangle committed Aug 19, 2024
1 parent b217a27 commit 4484573
Show file tree
Hide file tree
Showing 9 changed files with 113 additions and 108 deletions.
34 changes: 24 additions & 10 deletions packages/x-tree-view/src/TreeItem/TreeItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import { TreeItem2Provider } from '../TreeItem2Provider';
import { TreeViewItemDepthContext } from '../internals/TreeViewItemDepthContext';
import { useTreeItemState } from './useTreeItemState';
import { isTargetInDescendants } from '../internals/utils/tree';
import { TreeViewItemPluginSlotPropsEnhancerParams } from '@mui/x-tree-view/internals/models';

const useThemeProps = createUseThemeProps('MuiTreeItem');

Expand Down Expand Up @@ -221,8 +222,16 @@ export const TreeItem = React.forwardRef(function TreeItem(
...other
} = props;

const { expanded, focused, selected, disabled, editing, handleExpansion } =
useTreeItemState(itemId);
const {
expanded,
focused,
selected,
disabled,
editing,
handleExpansion,
handleCancelItemLabelEditing,
handleSaveItemLabel,
} = useTreeItemState(itemId);

const { contentRef, rootRef, propsEnhancers } = runItemPlugins<TreeItemProps>(props);
const rootRefObject = React.useRef<HTMLLIElement>(null);
Expand Down Expand Up @@ -375,28 +384,33 @@ export const TreeItem = React.forwardRef(function TreeItem(
const idAttribute = instance.getTreeItemIdAttribute(itemId, id);
const tabIndex = instance.canItemBeTabbed(itemId) ? 0 : -1;

const sharedPropsEnhancerParams: Omit<
TreeViewItemPluginSlotPropsEnhancerParams,
'externalEventHandlers'
> = {
rootRefObject,
contentRefObject,
interactions: { handleSaveItemLabel, handleCancelItemLabelEditing },
};

const enhancedRootProps =
propsEnhancers.root?.({
rootRefObject,
contentRefObject,
...sharedPropsEnhancerParams,
externalEventHandlers: extractEventHandlers(other),
}) ?? {};
const enhancedContentProps =
propsEnhancers.content?.({
rootRefObject,
contentRefObject,
...sharedPropsEnhancerParams,
externalEventHandlers: extractEventHandlers(ContentProps),
}) ?? {};
const enhancedDragAndDropOverlayProps =
propsEnhancers.dragAndDropOverlay?.({
rootRefObject,
contentRefObject,
...sharedPropsEnhancerParams,
externalEventHandlers: {},
}) ?? {};
const enhancedLabelInputProps =
propsEnhancers.labelInput?.({
rootRefObject,
contentRefObject,
...sharedPropsEnhancerParams,
externalEventHandlers: {},
}) ?? {};

Expand Down
33 changes: 1 addition & 32 deletions packages/x-tree-view/src/TreeItem/TreeItemContent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -144,32 +144,6 @@ const TreeItemContent = React.forwardRef(function TreeItemContent(
}
toggleItemEditing();
};
const handleLabelInputBlur = (
event: React.FocusEvent<HTMLInputElement> & MuiCancellableEvent,
) => {
if (event.defaultMuiPrevented) {
return;
}

if (event.target.value) {
handleSaveItemLabel(event, event.target.value);
}
};

const handleLabelInputKeydown = (
event: React.KeyboardEvent<HTMLInputElement> & MuiCancellableEvent,
) => {
if (event.defaultMuiPrevented) {
return;
}

const target = event.target as HTMLInputElement;
if (event.key === 'Enter' && target.value) {
handleSaveItemLabel(event, target.value);
} else if (event.key === 'Escape') {
handleCancelItemLabelEditing(event);
}
};

return (
/* eslint-disable-next-line jsx-a11y/click-events-have-key-events,jsx-a11y/no-static-element-interactions -- Key event is handled by the TreeView */
Expand Down Expand Up @@ -200,12 +174,7 @@ const TreeItemContent = React.forwardRef(function TreeItemContent(
)}

{editing ? (
<TreeItem2LabelInput
{...labelInputProps}
className={classes.labelInput}
onBlur={handleLabelInputBlur}
onKeyDown={handleLabelInputKeydown}
/>
<TreeItem2LabelInput {...labelInputProps} className={classes.labelInput} />
) : (
<div className={classes.label} {...(editable && { onDoubleClick: handleLabelDoubleClick })}>
{label}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
export interface TreeItem2LabelInputProps extends React.InputHTMLAttributes<HTMLInputElement> {
import * as React from 'react';
import { MuiCancellableEventHandler } from '../internals/models/MuiCancellableEvent';

export interface TreeItem2LabelInputProps {
value?: string;
onChange?: React.ChangeEventHandler<HTMLInputElement>;
/**
* Used to determine if the target of keydown or blur events is the input and prevent the event from propagating to the root.
*/
'data-element'?: 'labelInput';
onChange?: React.ChangeEventHandler<HTMLInputElement>;
onKeyDown?: MuiCancellableEventHandler<React.KeyboardEvent<HTMLInputElement>>;
onBlur?: MuiCancellableEventHandler<React.FocusEvent<HTMLInputElement>>;
autoFocus?: true;
type?: 'text';
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import {
import type { UseTreeItem2Status } from '../../useTreeItem2';
import { hasPlugin } from '../../internals/utils/plugins';

interface UseTreeItem2Interactions {
export interface UseTreeItem2Interactions {
handleExpansion: (event: React.MouseEvent) => void;
handleSelection: (event: React.MouseEvent) => void;
handleCheckboxSelection: (event: React.ChangeEvent<HTMLInputElement>) => void;
Expand Down
5 changes: 5 additions & 0 deletions packages/x-tree-view/src/internals/models/itemPlugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,16 @@ import type {
UseTreeItem2LabelInputSlotOwnProps,
UseTreeItem2RootSlotOwnProps,
} from '../../useTreeItem2';
import type { UseTreeItem2Interactions } from '../../hooks/useTreeItem2Utils/useTreeItem2Utils';

export interface TreeViewItemPluginSlotPropsEnhancerParams {
rootRefObject: React.MutableRefObject<HTMLLIElement | null>;
contentRefObject: React.MutableRefObject<HTMLDivElement | null>;
externalEventHandlers: EventHandlers;
interactions: Pick<
UseTreeItem2Interactions,
'handleSaveItemLabel' | 'handleCancelItemLabelEditing'
>;
}

type TreeViewItemPluginSlotPropsEnhancer<TSlotProps> = (
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,12 @@
import * as React from 'react';
import { useTreeViewContext } from '../../TreeViewProvider';
import { TreeViewItemPlugin } from '../../models';
import { MuiCancellableEvent, TreeViewItemPlugin } from '../../models';
import { UseTreeViewItemsSignature } from '../useTreeViewItems';
import {
UseTreeItem2LabelInputSlotPropsFromItemsReordering,
UseTreeItem2LabelInputSlotPropsFromLabelEditing,
UseTreeViewLabelSignature,
} from './useTreeViewLabel.types';

export const isAndroid = () => navigator.userAgent.toLowerCase().includes('android');

export const useTreeViewLabelItemPlugin: TreeViewItemPlugin<any> = ({ props }) => {
const { instance } = useTreeViewContext<[UseTreeViewItemsSignature, UseTreeViewLabelSignature]>();
const { label, itemId } = props;
Expand All @@ -27,13 +25,41 @@ export const useTreeViewLabelItemPlugin: TreeViewItemPlugin<any> = ({ props }) =
propsEnhancers: {
labelInput: ({
externalEventHandlers,
}): UseTreeItem2LabelInputSlotPropsFromItemsReordering => {
interactions,
}): UseTreeItem2LabelInputSlotPropsFromLabelEditing => {
const editable = instance.isItemEditable(itemId);

if (!editable) {
return {};
}

const handleKeydown = (
event: React.KeyboardEvent<HTMLInputElement> & MuiCancellableEvent,
) => {
externalEventHandlers.onKeyDown?.(event);
if (event.defaultMuiPrevented) {
return;
}
const target = event.target as HTMLInputElement;

if (event.key === 'Enter' && target.value) {
interactions.handleSaveItemLabel(event, target.value);
} else if (event.key === 'Escape') {
interactions.handleCancelItemLabelEditing(event);
}
};

const handleBlur = (event: React.FocusEvent<HTMLInputElement> & MuiCancellableEvent) => {
externalEventHandlers.onBlur?.(event);
if (event.defaultMuiPrevented) {
return;
}

if (event.target.value) {
interactions.handleSaveItemLabel(event, event.target.value);
}
};

const handleInputChange = (event: React.ChangeEvent<HTMLInputElement>) => {
externalEventHandlers.onChange?.(event);
setLabelInputValue(event.target.value);
Expand All @@ -43,6 +69,8 @@ export const useTreeViewLabelItemPlugin: TreeViewItemPlugin<any> = ({ props }) =
value: labelInputValue ?? '',
'data-element': 'labelInput',
onChange: handleInputChange,
onKeyDown: handleKeydown,
onBlur: handleBlur,
autoFocus: true,
type: 'text',
};
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { TreeViewPluginSignature } from '../../models';
import { MuiCancellableEventHandler, TreeViewPluginSignature } from '../../models';
import { TreeViewItemId } from '../../../models';
import { UseTreeViewItemsSignature } from '../useTreeViewItems';
import { TreeItem2LabelInputProps } from '../../../TreeItem2LabelInput';
import * as React from 'react';

export interface UseTreeViewLabelPublicAPI {
/**
Expand Down Expand Up @@ -76,5 +77,18 @@ export type UseTreeViewLabelSignature = TreeViewPluginSignature<{
experimentalFeatures: 'labelEditing';
dependencies: [UseTreeViewItemsSignature];
}>;
export interface UseTreeItem2LabelInputSlotPropsFromItemsReordering
extends TreeItem2LabelInputProps {}

export interface UseTreeItem2LabelInputSlotPropsFromLabelEditing extends TreeItem2LabelInputProps {
value?: string;
'data-element'?: 'labelInput';
onChange?: React.ChangeEventHandler<HTMLInputElement>;
onKeyDown?: MuiCancellableEventHandler<React.KeyboardEvent<HTMLInputElement>>;
onBlur?: MuiCancellableEventHandler<React.FocusEvent<HTMLInputElement>>;
autoFocus?: true;
type?: 'text';
}

declare module '@mui/x-tree-view/useTreeItem2' {
interface UseTreeItem2LabelInputSlotOwnProps
extends UseTreeItem2LabelInputSlotPropsFromLabelEditing {}
}
70 changes: 22 additions & 48 deletions packages/x-tree-view/src/useTreeItem2/useTreeItem2.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,10 @@ import {
UseTreeItem2ContentSlotPropsFromUseTreeItem,
} from './useTreeItem2.types';
import { useTreeViewContext } from '../internals/TreeViewProvider';
import { MuiCancellableEvent } from '../internals/models';
import {
MuiCancellableEvent,
TreeViewItemPluginSlotPropsEnhancerParams,
} from '../internals/models';
import { useTreeItem2Utils } from '../hooks/useTreeItem2Utils';
import { TreeViewItemDepthContext } from '../internals/TreeViewItemDepthContext';
import { isTargetInDescendants } from '../internals/utils/tree';
Expand Down Expand Up @@ -52,6 +55,11 @@ export const useTreeItem2 = <
const checkboxRef = React.useRef<HTMLButtonElement>(null);
const rootTabIndex = instance.canItemBeTabbed(itemId) ? 0 : -1;

const sharedPropsEnhancerParams: Omit<
TreeViewItemPluginSlotPropsEnhancerParams,
'externalEventHandlers'
> = { rootRefObject, contentRefObject, interactions };

const createRootHandleFocus =
(otherHandlers: EventHandlers) =>
(event: React.FocusEvent<HTMLElement> & MuiCancellableEvent) => {
Expand Down Expand Up @@ -164,35 +172,6 @@ export const useTreeItem2 = <
interactions.handleCheckboxSelection(event);
};

const createInputHandleKeydown =
(otherHandlers: EventHandlers) =>
(event: React.KeyboardEvent<HTMLInputElement> & MuiCancellableEvent) => {
otherHandlers.onKeyDown?.(event);
if (event.defaultMuiPrevented) {
return;
}
const target = event.target as HTMLInputElement;

if (event.key === 'Enter' && target.value) {
interactions.handleSaveItemLabel(event, target.value);
} else if (event.key === 'Escape') {
interactions.handleCancelItemLabelEditing(event);
}
};

const createInputHandleBlur =
(otherHandlers: EventHandlers) =>
(event: React.FocusEvent<HTMLInputElement> & MuiCancellableEvent) => {
otherHandlers.onBlur?.(event);
if (event.defaultMuiPrevented) {
return;
}

if (event.target.value) {
interactions.handleSaveItemLabel(event, event.target.value);
}
};

const createIconContainerHandleClick =
(otherHandlers: EventHandlers) => (event: React.MouseEvent & MuiCancellableEvent) => {
otherHandlers.onClick?.(event);
Expand Down Expand Up @@ -248,7 +227,7 @@ export const useTreeItem2 = <
}

const enhancedRootProps =
propsEnhancers.root?.({ rootRefObject, contentRefObject, externalEventHandlers }) ?? {};
propsEnhancers.root?.({ ...sharedPropsEnhancerParams, externalEventHandlers }) ?? {};

return {
...props,
Expand All @@ -275,7 +254,7 @@ export const useTreeItem2 = <
}

const enhancedContentProps =
propsEnhancers.content?.({ rootRefObject, contentRefObject, externalEventHandlers }) ?? {};
propsEnhancers.content?.({ ...sharedPropsEnhancerParams, externalEventHandlers }) ?? {};

return {
...props,
Expand Down Expand Up @@ -326,19 +305,17 @@ export const useTreeItem2 = <
): UseTreeItem2LabelInputSlotProps<ExternalProps> => {
const externalEventHandlers = extractEventHandlers(externalProps);

const props = {
...externalEventHandlers,
...externalProps,
onKeyDown: createInputHandleKeydown(externalEventHandlers),
onBlur: createInputHandleBlur(externalEventHandlers),
};

const enhancedlabelInputProps =
propsEnhancers.labelInput?.({ rootRefObject, contentRefObject, externalEventHandlers }) ?? {};
const enhancedLabelInputProps =
propsEnhancers.labelInput?.({
rootRefObject,
contentRefObject,
externalEventHandlers,
interactions,
}) ?? {};

return {
...props,
...enhancedlabelInputProps,
...externalProps,
...enhancedLabelInputProps,
} as UseTreeItem2LabelInputSlotProps<ExternalProps>;
};

Expand Down Expand Up @@ -379,14 +356,11 @@ export const useTreeItem2 = <
const getDragAndDropOverlayProps = <ExternalProps extends Record<string, any> = {}>(
externalProps: ExternalProps = {} as ExternalProps,
): UseTreeItem2DragAndDropOverlaySlotProps<ExternalProps> => {
const externalEventHandlers = {
...extractEventHandlers(externalProps),
};
const externalEventHandlers = extractEventHandlers(externalProps);

const enhancedDragAndDropOverlayProps =
propsEnhancers.dragAndDropOverlay?.({
rootRefObject,
contentRefObject,
...sharedPropsEnhancerParams,
externalEventHandlers,
}) ?? {};

Expand Down
5 changes: 1 addition & 4 deletions packages/x-tree-view/src/useTreeItem2/useTreeItem2.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,10 +92,7 @@ export interface UseTreeItem2LabelSlotOwnProps {
export type UseTreeItem2LabelSlotProps<ExternalProps = {}> = ExternalProps &
UseTreeItem2LabelSlotOwnProps;

export type UseTreeItem2LabelInputSlotOwnProps = {
onBlur: MuiCancellableEventHandler<React.FocusEvent<HTMLInputElement>>;
onKeyDown: MuiCancellableEventHandler<React.KeyboardEvent<HTMLInputElement>>;
};
export interface UseTreeItem2LabelInputSlotOwnProps {}

export type UseTreeItem2LabelInputSlotProps<ExternalProps = {}> = ExternalProps &
UseTreeItem2LabelInputSlotOwnProps;
Expand Down

0 comments on commit 4484573

Please sign in to comment.