From cdab9e007fc43c048a805152fa5c82501e6b628c Mon Sep 17 00:00:00 2001 From: delangle Date: Mon, 19 Aug 2024 15:56:40 +0200 Subject: [PATCH 1/4] [TreeView] Clean label editing code --- .../x-tree-view/src/TreeItem/TreeItem.tsx | 34 ++++++--- .../src/TreeItem/TreeItemContent.tsx | 33 +-------- .../TreeItem2LabelInput.types.ts | 14 ++-- .../useTreeItem2Utils/useTreeItem2Utils.tsx | 2 +- .../src/internals/models/itemPlugin.ts | 6 ++ .../useTreeViewLabel.itemPlugin.ts | 38 ++++++++-- .../useTreeViewLabel.types.ts | 9 ++- .../src/useTreeItem2/useTreeItem2.ts | 70 ++++++------------- .../src/useTreeItem2/useTreeItem2.types.ts | 5 +- 9 files changed, 104 insertions(+), 107 deletions(-) diff --git a/packages/x-tree-view/src/TreeItem/TreeItem.tsx b/packages/x-tree-view/src/TreeItem/TreeItem.tsx index 2be1daaa83f5..9892dff805c0 100644 --- a/packages/x-tree-view/src/TreeItem/TreeItem.tsx +++ b/packages/x-tree-view/src/TreeItem/TreeItem.tsx @@ -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'); @@ -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(props); const rootRefObject = React.useRef(null); @@ -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: {}, }) ?? {}; diff --git a/packages/x-tree-view/src/TreeItem/TreeItemContent.tsx b/packages/x-tree-view/src/TreeItem/TreeItemContent.tsx index 2c62bd29c452..d5c05febd359 100644 --- a/packages/x-tree-view/src/TreeItem/TreeItemContent.tsx +++ b/packages/x-tree-view/src/TreeItem/TreeItemContent.tsx @@ -144,32 +144,6 @@ const TreeItemContent = React.forwardRef(function TreeItemContent( } toggleItemEditing(); }; - const handleLabelInputBlur = ( - event: React.FocusEvent & MuiCancellableEvent, - ) => { - if (event.defaultMuiPrevented) { - return; - } - - if (event.target.value) { - handleSaveItemLabel(event, event.target.value); - } - }; - - const handleLabelInputKeydown = ( - event: React.KeyboardEvent & 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 */ @@ -200,12 +174,7 @@ const TreeItemContent = React.forwardRef(function TreeItemContent( )} {editing ? ( - + ) : (
{label} diff --git a/packages/x-tree-view/src/TreeItem2LabelInput/TreeItem2LabelInput.types.ts b/packages/x-tree-view/src/TreeItem2LabelInput/TreeItem2LabelInput.types.ts index 03e2414054e9..69b4f1d1d075 100644 --- a/packages/x-tree-view/src/TreeItem2LabelInput/TreeItem2LabelInput.types.ts +++ b/packages/x-tree-view/src/TreeItem2LabelInput/TreeItem2LabelInput.types.ts @@ -1,8 +1,12 @@ -export interface TreeItem2LabelInputProps extends React.InputHTMLAttributes { +import * as React from 'react'; +import { MuiCancellableEventHandler } from '../internals/models/MuiCancellableEvent'; + +export interface TreeItem2LabelInputProps { value?: string; - onChange?: React.ChangeEventHandler; - /** - * 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; + onKeyDown?: MuiCancellableEventHandler>; + onBlur?: MuiCancellableEventHandler>; + autoFocus?: true; + type?: 'text'; } diff --git a/packages/x-tree-view/src/hooks/useTreeItem2Utils/useTreeItem2Utils.tsx b/packages/x-tree-view/src/hooks/useTreeItem2Utils/useTreeItem2Utils.tsx index fb2d994d28c6..245633ecf95e 100644 --- a/packages/x-tree-view/src/hooks/useTreeItem2Utils/useTreeItem2Utils.tsx +++ b/packages/x-tree-view/src/hooks/useTreeItem2Utils/useTreeItem2Utils.tsx @@ -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) => void; diff --git a/packages/x-tree-view/src/internals/models/itemPlugin.ts b/packages/x-tree-view/src/internals/models/itemPlugin.ts index ad02c708dcff..c0858f84d960 100644 --- a/packages/x-tree-view/src/internals/models/itemPlugin.ts +++ b/packages/x-tree-view/src/internals/models/itemPlugin.ts @@ -6,11 +6,17 @@ import type { UseTreeItem2LabelInputSlotOwnProps, UseTreeItem2RootSlotOwnProps, } from '../../useTreeItem2'; +import type { UseTreeItem2Interactions } from '../../hooks/useTreeItem2Utils/useTreeItem2Utils'; export interface TreeViewItemPluginSlotPropsEnhancerParams { rootRefObject: React.MutableRefObject; contentRefObject: React.MutableRefObject; externalEventHandlers: EventHandlers; + // TODO v9: Remove "Pick" once the old TreeItem is removed. + interactions: Pick< + UseTreeItem2Interactions, + 'handleSaveItemLabel' | 'handleCancelItemLabelEditing' + >; } type TreeViewItemPluginSlotPropsEnhancer = ( diff --git a/packages/x-tree-view/src/internals/plugins/useTreeViewLabel/useTreeViewLabel.itemPlugin.ts b/packages/x-tree-view/src/internals/plugins/useTreeViewLabel/useTreeViewLabel.itemPlugin.ts index 96e143e02dbf..3a663be337b8 100644 --- a/packages/x-tree-view/src/internals/plugins/useTreeViewLabel/useTreeViewLabel.itemPlugin.ts +++ b/packages/x-tree-view/src/internals/plugins/useTreeViewLabel/useTreeViewLabel.itemPlugin.ts @@ -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 = ({ props }) => { const { instance } = useTreeViewContext<[UseTreeViewItemsSignature, UseTreeViewLabelSignature]>(); const { label, itemId } = props; @@ -27,13 +25,41 @@ export const useTreeViewLabelItemPlugin: TreeViewItemPlugin = ({ props }) = propsEnhancers: { labelInput: ({ externalEventHandlers, - }): UseTreeItem2LabelInputSlotPropsFromItemsReordering => { + interactions, + }): UseTreeItem2LabelInputSlotPropsFromLabelEditing => { const editable = instance.isItemEditable(itemId); if (!editable) { return {}; } + const handleKeydown = ( + event: React.KeyboardEvent & 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 & MuiCancellableEvent) => { + externalEventHandlers.onBlur?.(event); + if (event.defaultMuiPrevented) { + return; + } + + if (event.target.value) { + interactions.handleSaveItemLabel(event, event.target.value); + } + }; + const handleInputChange = (event: React.ChangeEvent) => { externalEventHandlers.onChange?.(event); setLabelInputValue(event.target.value); @@ -43,6 +69,8 @@ export const useTreeViewLabelItemPlugin: TreeViewItemPlugin = ({ props }) = value: labelInputValue ?? '', 'data-element': 'labelInput', onChange: handleInputChange, + onKeyDown: handleKeydown, + onBlur: handleBlur, autoFocus: true, type: 'text', }; diff --git a/packages/x-tree-view/src/internals/plugins/useTreeViewLabel/useTreeViewLabel.types.ts b/packages/x-tree-view/src/internals/plugins/useTreeViewLabel/useTreeViewLabel.types.ts index 37a348a2da61..12f5296ef61a 100644 --- a/packages/x-tree-view/src/internals/plugins/useTreeViewLabel/useTreeViewLabel.types.ts +++ b/packages/x-tree-view/src/internals/plugins/useTreeViewLabel/useTreeViewLabel.types.ts @@ -76,5 +76,10 @@ export type UseTreeViewLabelSignature = TreeViewPluginSignature<{ experimentalFeatures: 'labelEditing'; dependencies: [UseTreeViewItemsSignature]; }>; -export interface UseTreeItem2LabelInputSlotPropsFromItemsReordering - extends TreeItem2LabelInputProps {} + +export interface UseTreeItem2LabelInputSlotPropsFromLabelEditing extends TreeItem2LabelInputProps {} + +declare module '@mui/x-tree-view/useTreeItem2' { + interface UseTreeItem2LabelInputSlotOwnProps + extends UseTreeItem2LabelInputSlotPropsFromLabelEditing {} +} diff --git a/packages/x-tree-view/src/useTreeItem2/useTreeItem2.ts b/packages/x-tree-view/src/useTreeItem2/useTreeItem2.ts index e9424c9d28f1..64c696d245f0 100644 --- a/packages/x-tree-view/src/useTreeItem2/useTreeItem2.ts +++ b/packages/x-tree-view/src/useTreeItem2/useTreeItem2.ts @@ -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'; @@ -52,6 +55,11 @@ export const useTreeItem2 = < const checkboxRef = React.useRef(null); const rootTabIndex = instance.canItemBeTabbed(itemId) ? 0 : -1; + const sharedPropsEnhancerParams: Omit< + TreeViewItemPluginSlotPropsEnhancerParams, + 'externalEventHandlers' + > = { rootRefObject, contentRefObject, interactions }; + const createRootHandleFocus = (otherHandlers: EventHandlers) => (event: React.FocusEvent & MuiCancellableEvent) => { @@ -164,35 +172,6 @@ export const useTreeItem2 = < interactions.handleCheckboxSelection(event); }; - const createInputHandleKeydown = - (otherHandlers: EventHandlers) => - (event: React.KeyboardEvent & 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 & 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); @@ -248,7 +227,7 @@ export const useTreeItem2 = < } const enhancedRootProps = - propsEnhancers.root?.({ rootRefObject, contentRefObject, externalEventHandlers }) ?? {}; + propsEnhancers.root?.({ ...sharedPropsEnhancerParams, externalEventHandlers }) ?? {}; return { ...props, @@ -275,7 +254,7 @@ export const useTreeItem2 = < } const enhancedContentProps = - propsEnhancers.content?.({ rootRefObject, contentRefObject, externalEventHandlers }) ?? {}; + propsEnhancers.content?.({ ...sharedPropsEnhancerParams, externalEventHandlers }) ?? {}; return { ...props, @@ -326,19 +305,17 @@ export const useTreeItem2 = < ): UseTreeItem2LabelInputSlotProps => { 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; }; @@ -379,14 +356,11 @@ export const useTreeItem2 = < const getDragAndDropOverlayProps = = {}>( externalProps: ExternalProps = {} as ExternalProps, ): UseTreeItem2DragAndDropOverlaySlotProps => { - const externalEventHandlers = { - ...extractEventHandlers(externalProps), - }; + const externalEventHandlers = extractEventHandlers(externalProps); const enhancedDragAndDropOverlayProps = propsEnhancers.dragAndDropOverlay?.({ - rootRefObject, - contentRefObject, + ...sharedPropsEnhancerParams, externalEventHandlers, }) ?? {}; diff --git a/packages/x-tree-view/src/useTreeItem2/useTreeItem2.types.ts b/packages/x-tree-view/src/useTreeItem2/useTreeItem2.types.ts index e59f98ab285a..9457cc157250 100644 --- a/packages/x-tree-view/src/useTreeItem2/useTreeItem2.types.ts +++ b/packages/x-tree-view/src/useTreeItem2/useTreeItem2.types.ts @@ -92,10 +92,7 @@ export interface UseTreeItem2LabelSlotOwnProps { export type UseTreeItem2LabelSlotProps = ExternalProps & UseTreeItem2LabelSlotOwnProps; -export type UseTreeItem2LabelInputSlotOwnProps = { - onBlur: MuiCancellableEventHandler>; - onKeyDown: MuiCancellableEventHandler>; -}; +export interface UseTreeItem2LabelInputSlotOwnProps {} export type UseTreeItem2LabelInputSlotProps = ExternalProps & UseTreeItem2LabelInputSlotOwnProps; From 5070e7d15d4d87a05d354c5d99c0e4b531f9a371 Mon Sep 17 00:00:00 2001 From: delangle Date: Mon, 19 Aug 2024 16:12:05 +0200 Subject: [PATCH 2/4] Fix CI --- packages/x-tree-view/src/TreeItem/TreeItem.tsx | 2 +- .../x-tree-view/src/TreeItem/TreeItemContent.tsx | 12 +++++++++--- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/packages/x-tree-view/src/TreeItem/TreeItem.tsx b/packages/x-tree-view/src/TreeItem/TreeItem.tsx index 9892dff805c0..6831bcee8055 100644 --- a/packages/x-tree-view/src/TreeItem/TreeItem.tsx +++ b/packages/x-tree-view/src/TreeItem/TreeItem.tsx @@ -27,7 +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'; +import { TreeViewItemPluginSlotPropsEnhancerParams } from '../internals/models'; const useThemeProps = createUseThemeProps('MuiTreeItem'); diff --git a/packages/x-tree-view/src/TreeItem/TreeItemContent.tsx b/packages/x-tree-view/src/TreeItem/TreeItemContent.tsx index d5c05febd359..247c593f687d 100644 --- a/packages/x-tree-view/src/TreeItem/TreeItemContent.tsx +++ b/packages/x-tree-view/src/TreeItem/TreeItemContent.tsx @@ -103,8 +103,6 @@ const TreeItemContent = React.forwardRef(function TreeItemContent( preventSelection, expansionTrigger, toggleItemEditing, - handleSaveItemLabel, - handleCancelItemLabelEditing, } = useTreeItemState(itemId); const icon = iconProp || expansionIcon || displayIcon; @@ -220,7 +218,15 @@ TreeItemContent.propTypes = { * The tree item label. */ label: PropTypes.node, - labelInputProps: PropTypes.object, + labelInputProps: PropTypes.shape({ + autoFocus: PropTypes.oneOf([true]), + 'data-element': PropTypes.oneOf(['labelInput']), + onBlur: PropTypes.func, + onChange: PropTypes.func, + onKeyDown: PropTypes.func, + type: PropTypes.oneOf(['text']), + value: PropTypes.string, + }), } as any; export { TreeItemContent }; From 10824181f3450abec8c197a757494810b1ac88c9 Mon Sep 17 00:00:00 2001 From: delangle Date: Mon, 19 Aug 2024 17:47:37 +0200 Subject: [PATCH 3/4] Fix CI --- scripts/x-tree-view-pro.exports.json | 2 +- scripts/x-tree-view.exports.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/x-tree-view-pro.exports.json b/scripts/x-tree-view-pro.exports.json index 1db2fafc7962..16f61d3c4727 100644 --- a/scripts/x-tree-view-pro.exports.json +++ b/scripts/x-tree-view-pro.exports.json @@ -66,7 +66,7 @@ { "name": "UseTreeItem2DragAndDropOverlaySlotOwnProps", "kind": "Interface" }, { "name": "UseTreeItem2GroupTransitionSlotOwnProps", "kind": "Interface" }, { "name": "UseTreeItem2IconContainerSlotOwnProps", "kind": "Interface" }, - { "name": "UseTreeItem2LabelInputSlotOwnProps", "kind": "TypeAlias" }, + { "name": "UseTreeItem2LabelInputSlotOwnProps", "kind": "Interface" }, { "name": "UseTreeItem2LabelSlotOwnProps", "kind": "Interface" }, { "name": "UseTreeItem2Parameters", "kind": "Interface" }, { "name": "UseTreeItem2ReturnValue", "kind": "Interface" }, diff --git a/scripts/x-tree-view.exports.json b/scripts/x-tree-view.exports.json index 7599088f3010..49a0d1ae5a7b 100644 --- a/scripts/x-tree-view.exports.json +++ b/scripts/x-tree-view.exports.json @@ -70,7 +70,7 @@ { "name": "UseTreeItem2DragAndDropOverlaySlotOwnProps", "kind": "Interface" }, { "name": "UseTreeItem2GroupTransitionSlotOwnProps", "kind": "Interface" }, { "name": "UseTreeItem2IconContainerSlotOwnProps", "kind": "Interface" }, - { "name": "UseTreeItem2LabelInputSlotOwnProps", "kind": "TypeAlias" }, + { "name": "UseTreeItem2LabelInputSlotOwnProps", "kind": "Interface" }, { "name": "UseTreeItem2LabelSlotOwnProps", "kind": "Interface" }, { "name": "UseTreeItem2Parameters", "kind": "Interface" }, { "name": "UseTreeItem2ReturnValue", "kind": "Interface" }, From 1aeb92f0d21970f809f7c6df88f5d622aa34e509 Mon Sep 17 00:00:00 2001 From: delangle Date: Mon, 2 Sep 2024 10:55:07 +0200 Subject: [PATCH 4/4] Review Nora --- .../src/TreeItem2LabelInput/TreeItem2LabelInput.types.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/x-tree-view/src/TreeItem2LabelInput/TreeItem2LabelInput.types.ts b/packages/x-tree-view/src/TreeItem2LabelInput/TreeItem2LabelInput.types.ts index 69b4f1d1d075..099b8c5b2b5e 100644 --- a/packages/x-tree-view/src/TreeItem2LabelInput/TreeItem2LabelInput.types.ts +++ b/packages/x-tree-view/src/TreeItem2LabelInput/TreeItem2LabelInput.types.ts @@ -3,6 +3,9 @@ import { MuiCancellableEventHandler } from '../internals/models/MuiCancellableEv export interface TreeItem2LabelInputProps { value?: string; + /** + * 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; onKeyDown?: MuiCancellableEventHandler>;