diff --git a/docs/data/charts/lines/AreaBaseline.js b/docs/data/charts/lines/AreaBaseline.js new file mode 100644 index 000000000000..cbaa2965c408 --- /dev/null +++ b/docs/data/charts/lines/AreaBaseline.js @@ -0,0 +1,19 @@ +import * as React from 'react'; +import { LineChart } from '@mui/x-charts/LineChart'; + +export default function AreaBaseline() { + return ( + + ); +} diff --git a/docs/data/charts/lines/AreaBaseline.tsx b/docs/data/charts/lines/AreaBaseline.tsx new file mode 100644 index 000000000000..cbaa2965c408 --- /dev/null +++ b/docs/data/charts/lines/AreaBaseline.tsx @@ -0,0 +1,19 @@ +import * as React from 'react'; +import { LineChart } from '@mui/x-charts/LineChart'; + +export default function AreaBaseline() { + return ( + + ); +} diff --git a/docs/data/charts/lines/AreaBaseline.tsx.preview b/docs/data/charts/lines/AreaBaseline.tsx.preview new file mode 100644 index 000000000000..b5f56b85ada1 --- /dev/null +++ b/docs/data/charts/lines/AreaBaseline.tsx.preview @@ -0,0 +1,12 @@ + \ No newline at end of file diff --git a/docs/data/charts/lines/lines.md b/docs/data/charts/lines/lines.md index 0ad25ce2f397..be651d4dd57a 100644 --- a/docs/data/charts/lines/lines.md +++ b/docs/data/charts/lines/lines.md @@ -175,6 +175,20 @@ Different series could even have different interpolations. {{"demo": "InterpolationDemoNoSnap.js", "hideToolbar": true}} +### Baseline + +The area chart draws a `baseline` on the Y axis `0`. +This is useful as a base value, but customized visualizations may require a different baseline. + +To get the area filling the space above or below the line, set `baseline` to `"min"` or `"max"`. +It is also possible to provide a `number` value to fix the baseline at the desired position. + +:::warning +The `baseline` should not be used with stacked areas, as it will not work as expected. +::: + +{{"demo": "AreaBaseline.js"}} + ### Optimization To show mark elements, use `showMark` series property. diff --git a/docs/data/pages.ts b/docs/data/pages.ts index 950d5f43e5c4..46c9fed9e4c2 100644 --- a/docs/data/pages.ts +++ b/docs/data/pages.ts @@ -516,6 +516,7 @@ const pages: MuiPage[] = [ { pathname: '/x/react-tree-view/rich-tree-view/expansion' }, { pathname: '/x/react-tree-view/rich-tree-view/customization' }, { pathname: '/x/react-tree-view/rich-tree-view/focus' }, + { pathname: '/x/react-tree-view/rich-tree-view/editing' }, { pathname: '/x/react-tree-view/rich-tree-view/ordering', plan: 'pro' }, ], }, diff --git a/docs/data/tree-view/rich-tree-view/editing/ApiMethodUpdateItemLabel.js b/docs/data/tree-view/rich-tree-view/editing/ApiMethodUpdateItemLabel.js new file mode 100644 index 000000000000..6d8aff8043d6 --- /dev/null +++ b/docs/data/tree-view/rich-tree-view/editing/ApiMethodUpdateItemLabel.js @@ -0,0 +1,35 @@ +import * as React from 'react'; +import Box from '@mui/material/Box'; +import Stack from '@mui/material/Stack'; +import Button from '@mui/material/Button'; +import { RichTreeView } from '@mui/x-tree-view/RichTreeView'; +import { useTreeViewApiRef } from '@mui/x-tree-view/hooks'; +import { MUI_X_PRODUCTS } from './products'; + +export default function ApiMethodUpdateItemLabel() { + const [isLabelUpdated, setIsLabelUpdated] = React.useState(false); + const apiRef = useTreeViewApiRef(); + + const handleUpdateLabel = () => { + if (isLabelUpdated) { + apiRef.current.updateItemLabel('grid', 'Data Grid'); + setIsLabelUpdated(false); + } else { + apiRef.current.updateItemLabel('grid', 'New Label'); + setIsLabelUpdated(true); + } + }; + + return ( + + + + + + + + + ); +} diff --git a/docs/data/tree-view/rich-tree-view/editing/ApiMethodUpdateItemLabel.tsx b/docs/data/tree-view/rich-tree-view/editing/ApiMethodUpdateItemLabel.tsx new file mode 100644 index 000000000000..69a4fb59308f --- /dev/null +++ b/docs/data/tree-view/rich-tree-view/editing/ApiMethodUpdateItemLabel.tsx @@ -0,0 +1,35 @@ +import * as React from 'react'; +import Box from '@mui/material/Box'; +import Stack from '@mui/material/Stack'; +import Button from '@mui/material/Button'; +import { RichTreeView } from '@mui/x-tree-view/RichTreeView'; +import { useTreeViewApiRef } from '@mui/x-tree-view/hooks'; +import { MUI_X_PRODUCTS } from './products'; + +export default function ApiMethodUpdateItemLabel() { + const [isLabelUpdated, setIsLabelUpdated] = React.useState(false); + const apiRef = useTreeViewApiRef(); + + const handleUpdateLabel = () => { + if (isLabelUpdated) { + apiRef.current!.updateItemLabel('grid', 'Data Grid'); + setIsLabelUpdated(false); + } else { + apiRef.current!.updateItemLabel('grid', 'New Label'); + setIsLabelUpdated(true); + } + }; + + return ( + + + + + + + + + ); +} diff --git a/docs/data/tree-view/rich-tree-view/editing/ApiMethodUpdateItemLabel.tsx.preview b/docs/data/tree-view/rich-tree-view/editing/ApiMethodUpdateItemLabel.tsx.preview new file mode 100644 index 000000000000..e10c7e1b68f4 --- /dev/null +++ b/docs/data/tree-view/rich-tree-view/editing/ApiMethodUpdateItemLabel.tsx.preview @@ -0,0 +1,8 @@ + + + + + + \ No newline at end of file diff --git a/docs/data/tree-view/rich-tree-view/editing/CustomBehavior.js b/docs/data/tree-view/rich-tree-view/editing/CustomBehavior.js new file mode 100644 index 000000000000..4c52e4c94ec8 --- /dev/null +++ b/docs/data/tree-view/rich-tree-view/editing/CustomBehavior.js @@ -0,0 +1,44 @@ +import * as React from 'react'; +import Box from '@mui/material/Box'; +import { RichTreeView } from '@mui/x-tree-view/RichTreeView'; +import { useTreeItem2Utils } from '@mui/x-tree-view/hooks'; +import { TreeItem2 } from '@mui/x-tree-view/TreeItem2'; + +import { MUI_X_PRODUCTS } from './products'; + +const CustomTreeItem2 = React.forwardRef(function CustomTreeItem2(props, ref) { + const { interactions } = useTreeItem2Utils({ + itemId: props.itemId, + children: props.children, + }); + + const handleInputBlur = (event) => { + interactions.handleCancelItemLabelEditing(event); + }; + + return ( + + ); +}); + +export default function CustomBehavior() { + return ( + + + + ); +} diff --git a/docs/data/tree-view/rich-tree-view/editing/CustomBehavior.tsx b/docs/data/tree-view/rich-tree-view/editing/CustomBehavior.tsx new file mode 100644 index 000000000000..b8b44e50cca6 --- /dev/null +++ b/docs/data/tree-view/rich-tree-view/editing/CustomBehavior.tsx @@ -0,0 +1,47 @@ +import * as React from 'react'; +import Box from '@mui/material/Box'; +import { RichTreeView } from '@mui/x-tree-view/RichTreeView'; +import { useTreeItem2Utils } from '@mui/x-tree-view/hooks'; +import { TreeItem2, TreeItem2Props } from '@mui/x-tree-view/TreeItem2'; +import { UseTreeItem2LabelInputSlotOwnProps } from '@mui/x-tree-view/useTreeItem2'; +import { MUI_X_PRODUCTS } from './products'; + +const CustomTreeItem2 = React.forwardRef(function CustomTreeItem2( + props: TreeItem2Props, + ref: React.Ref, +) { + const { interactions } = useTreeItem2Utils({ + itemId: props.itemId, + children: props.children, + }); + + const handleInputBlur: UseTreeItem2LabelInputSlotOwnProps['onBlur'] = (event) => { + interactions.handleCancelItemLabelEditing(event); + }; + + return ( + + ); +}); + +export default function CustomBehavior() { + return ( + + + + ); +} diff --git a/docs/data/tree-view/rich-tree-view/editing/CustomBehavior.tsx.preview b/docs/data/tree-view/rich-tree-view/editing/CustomBehavior.tsx.preview new file mode 100644 index 000000000000..c8745d2d8df8 --- /dev/null +++ b/docs/data/tree-view/rich-tree-view/editing/CustomBehavior.tsx.preview @@ -0,0 +1,7 @@ + \ No newline at end of file diff --git a/docs/data/tree-view/rich-tree-view/editing/CustomLabelInput.js b/docs/data/tree-view/rich-tree-view/editing/CustomLabelInput.js new file mode 100644 index 000000000000..5433087dfed1 --- /dev/null +++ b/docs/data/tree-view/rich-tree-view/editing/CustomLabelInput.js @@ -0,0 +1,185 @@ +import * as React from 'react'; +import { styled } from '@mui/material/styles'; +import Box from '@mui/material/Box'; +import CheckIcon from '@mui/icons-material/Check'; +import IconButton from '@mui/material/IconButton'; +import { RichTreeView } from '@mui/x-tree-view/RichTreeView'; +import CloseRoundedIcon from '@mui/icons-material/CloseRounded'; +import { TreeItem2, TreeItem2Label } from '@mui/x-tree-view/TreeItem2'; +import { unstable_useTreeItem2 as useTreeItem2 } from '@mui/x-tree-view/useTreeItem2'; +import { useTreeItem2Utils } from '@mui/x-tree-view/hooks/useTreeItem2Utils'; + +const StyledLabelInput = styled('input')(({ theme }) => ({ + ...theme.typography.body1, + backgroundColor: theme.palette.background.paper, + borderRadius: theme.shape.borderRadius, + border: 'none', + padding: '0 2px', + boxSizing: 'border-box', + width: 100, + '&:focus': { + outline: `1px solid ${theme.palette.primary.main}`, + }, +})); + +export const ITEMS = [ + { + id: '1', + firstName: 'Jane', + lastName: 'Doe', + editable: true, + children: [ + { id: '1.1', firstName: 'Elena', lastName: 'Kim', editable: true }, + { id: '1.2', firstName: 'Noah', lastName: 'Rodriguez', editable: true }, + { id: '1.3', firstName: 'Maya', lastName: 'Patel', editable: true }, + ], + }, + { + id: '2', + firstName: 'Liam', + lastName: 'Clarke', + editable: true, + children: [ + { + id: '2.1', + firstName: 'Ethan', + lastName: 'Lee', + editable: true, + }, + { id: '2.2', firstName: 'Ava', lastName: 'Jones', editable: true }, + ], + }, +]; + +function Label({ children, ...other }) { + return ( + + {children} + + ); +} + +const LabelInput = React.forwardRef(function LabelInput( + { item, handleCancelItemLabelEditing, handleSaveItemLabel, ...props }, + ref, +) { + const [initialNameValue, setInitialNameValue] = React.useState({ + firstName: item.firstName, + lastName: item.lastName, + }); + const [nameValue, setNameValue] = React.useState({ + firstName: item.firstName, + lastName: item.lastName, + }); + + const handleFirstNameChange = (event) => { + setNameValue((prev) => ({ ...prev, firstName: event.target.value })); + }; + const handleLastNameChange = (event) => { + setNameValue((prev) => ({ ...prev, lastName: event.target.value })); + }; + + const reset = () => { + setNameValue(initialNameValue); + }; + const save = () => { + setInitialNameValue(nameValue); + }; + + return ( + + + + { + handleSaveItemLabel(event, `${nameValue.firstName} ${nameValue.lastName}`); + save(); + }} + > + + + { + handleCancelItemLabelEditing(event); + reset(); + }} + > + + + + ); +}); + +const CustomTreeItem2 = React.forwardRef(function CustomTreeItem2(props, ref) { + const { interactions } = useTreeItem2Utils({ + itemId: props.itemId, + children: props.children, + }); + const { publicAPI } = useTreeItem2(props); + + const handleInputBlur = (event) => { + event.defaultMuiPrevented = true; + }; + + const handleInputKeyDown = (event) => { + event.defaultMuiPrevented = true; + }; + + return ( + + ); +}); + +export default function CustomLabelInput() { + return ( + + `${item.firstName} ${item.lastName}`} + /> + + ); +} diff --git a/docs/data/tree-view/rich-tree-view/editing/CustomLabelInput.tsx b/docs/data/tree-view/rich-tree-view/editing/CustomLabelInput.tsx new file mode 100644 index 000000000000..188bb9943aa0 --- /dev/null +++ b/docs/data/tree-view/rich-tree-view/editing/CustomLabelInput.tsx @@ -0,0 +1,217 @@ +import * as React from 'react'; +import { styled } from '@mui/material/styles'; +import Box from '@mui/material/Box'; +import CheckIcon from '@mui/icons-material/Check'; +import IconButton from '@mui/material/IconButton'; +import { RichTreeView } from '@mui/x-tree-view/RichTreeView'; +import CloseRoundedIcon from '@mui/icons-material/CloseRounded'; +import { + TreeItem2, + TreeItem2Label, + TreeItem2Props, +} from '@mui/x-tree-view/TreeItem2'; +import { + UseTreeItem2LabelInputSlotOwnProps, + UseTreeItem2LabelSlotOwnProps, + unstable_useTreeItem2 as useTreeItem2, +} from '@mui/x-tree-view/useTreeItem2'; +import { useTreeItem2Utils } from '@mui/x-tree-view/hooks/useTreeItem2Utils'; +import { TreeViewBaseItem } from '@mui/x-tree-view/models'; + +const StyledLabelInput = styled('input')(({ theme }) => ({ + ...theme.typography.body1, + backgroundColor: theme.palette.background.paper, + borderRadius: theme.shape.borderRadius, + border: 'none', + padding: '0 2px', + boxSizing: 'border-box', + width: 100, + '&:focus': { + outline: `1px solid ${theme.palette.primary.main}`, + }, +})); + +type ExtendedTreeItemProps = { + editable?: boolean; + id: string; + firstName: string; + lastName: string; +}; + +export const ITEMS: TreeViewBaseItem[] = [ + { + id: '1', + firstName: 'Jane', + lastName: 'Doe', + editable: true, + children: [ + { id: '1.1', firstName: 'Elena', lastName: 'Kim', editable: true }, + { id: '1.2', firstName: 'Noah', lastName: 'Rodriguez', editable: true }, + { id: '1.3', firstName: 'Maya', lastName: 'Patel', editable: true }, + ], + }, + { + id: '2', + firstName: 'Liam', + lastName: 'Clarke', + editable: true, + children: [ + { + id: '2.1', + firstName: 'Ethan', + lastName: 'Lee', + editable: true, + }, + { id: '2.2', firstName: 'Ava', lastName: 'Jones', editable: true }, + ], + }, +]; + +function Label({ children, ...other }: UseTreeItem2LabelSlotOwnProps) { + return ( + + {children} + + ); +} + +interface CustomLabelInputProps extends UseTreeItem2LabelInputSlotOwnProps { + handleCancelItemLabelEditing: (event: React.SyntheticEvent) => void; + handleSaveItemLabel: (event: React.SyntheticEvent, label: string) => void; + item: TreeViewBaseItem; +} + +const LabelInput = React.forwardRef(function LabelInput( + { + item, + handleCancelItemLabelEditing, + handleSaveItemLabel, + ...props + }: Omit, + ref: React.Ref, +) { + const [initialNameValue, setInitialNameValue] = React.useState({ + firstName: item.firstName, + lastName: item.lastName, + }); + const [nameValue, setNameValue] = React.useState({ + firstName: item.firstName, + lastName: item.lastName, + }); + + const handleFirstNameChange = (event: React.ChangeEvent) => { + setNameValue((prev) => ({ ...prev, firstName: event.target.value })); + }; + const handleLastNameChange = (event: React.ChangeEvent) => { + setNameValue((prev) => ({ ...prev, lastName: event.target.value })); + }; + + const reset = () => { + setNameValue(initialNameValue); + }; + const save = () => { + setInitialNameValue(nameValue); + }; + + return ( + + + + { + handleSaveItemLabel(event, `${nameValue.firstName} ${nameValue.lastName}`); + save(); + }} + > + + + { + handleCancelItemLabelEditing(event); + reset(); + }} + > + + + + ); +}); + +const CustomTreeItem2 = React.forwardRef(function CustomTreeItem2( + props: TreeItem2Props, + ref: React.Ref, +) { + const { interactions } = useTreeItem2Utils({ + itemId: props.itemId, + children: props.children, + }); + const { publicAPI } = useTreeItem2(props); + + const handleInputBlur: UseTreeItem2LabelInputSlotOwnProps['onBlur'] = (event) => { + event.defaultMuiPrevented = true; + }; + + const handleInputKeyDown: UseTreeItem2LabelInputSlotOwnProps['onKeyDown'] = ( + event, + ) => { + event.defaultMuiPrevented = true; + }; + + return ( + + ); +}); + +export default function CustomLabelInput() { + return ( + + `${item.firstName} ${item.lastName}`} + /> + + ); +} diff --git a/docs/data/tree-view/rich-tree-view/editing/CustomLabelInput.tsx.preview b/docs/data/tree-view/rich-tree-view/editing/CustomLabelInput.tsx.preview new file mode 100644 index 000000000000..9712852c253a --- /dev/null +++ b/docs/data/tree-view/rich-tree-view/editing/CustomLabelInput.tsx.preview @@ -0,0 +1,8 @@ + `${item.firstName} ${item.lastName}`} +/> \ No newline at end of file diff --git a/docs/data/tree-view/rich-tree-view/editing/EditLeaves.js b/docs/data/tree-view/rich-tree-view/editing/EditLeaves.js new file mode 100644 index 000000000000..041b88409974 --- /dev/null +++ b/docs/data/tree-view/rich-tree-view/editing/EditLeaves.js @@ -0,0 +1,54 @@ +import * as React from 'react'; +import Box from '@mui/material/Box'; +import { RichTreeView } from '@mui/x-tree-view/RichTreeView'; +import { useTreeViewApiRef } from '@mui/x-tree-view/hooks'; + +const MUI_X_PRODUCTS = [ + { + id: 'grid', + label: 'Data Grid', + children: [ + { id: 'grid-community', label: '@mui/x-data-grid' }, + { id: 'grid-pro', label: '@mui/x-data-grid-pro' }, + { id: 'grid-premium', label: '@mui/x-data-grid-premium' }, + ], + }, + { + id: 'pickers', + label: 'Date and time pickers', + children: [ + { + id: 'pickers-community', + label: '@mui/x-date-pickers', + }, + { id: 'pickers-pro', label: '@mui/x-date-pickers-pro' }, + ], + }, + { + id: 'charts', + label: 'Charts', + children: [{ id: 'charts-community', label: '@mui/x-charts' }], + }, + { + id: 'tree-view', + label: 'Tree View', + children: [{ id: 'tree-view-community', label: '@mui/x-tree-view' }], + }, +]; + +export default function EditLeaves() { + const apiRef = useTreeViewApiRef(); + return ( + + + apiRef.current.getItemOrderedChildrenIds(item.id).length === 0 + } + defaultExpandedItems={['grid', 'pickers']} + /> + + ); +} diff --git a/docs/data/tree-view/rich-tree-view/editing/EditLeaves.tsx b/docs/data/tree-view/rich-tree-view/editing/EditLeaves.tsx new file mode 100644 index 000000000000..6d1a2ef9997e --- /dev/null +++ b/docs/data/tree-view/rich-tree-view/editing/EditLeaves.tsx @@ -0,0 +1,61 @@ +import * as React from 'react'; +import Box from '@mui/material/Box'; +import { RichTreeView } from '@mui/x-tree-view/RichTreeView'; +import { useTreeViewApiRef } from '@mui/x-tree-view/hooks'; +import { TreeViewBaseItem } from '@mui/x-tree-view/models'; + +type ExtendedTreeItemProps = { + editable?: boolean; + id: string; + label: string; +}; + +const MUI_X_PRODUCTS: TreeViewBaseItem[] = [ + { + id: 'grid', + label: 'Data Grid', + children: [ + { id: 'grid-community', label: '@mui/x-data-grid' }, + { id: 'grid-pro', label: '@mui/x-data-grid-pro' }, + { id: 'grid-premium', label: '@mui/x-data-grid-premium' }, + ], + }, + { + id: 'pickers', + label: 'Date and time pickers', + children: [ + { + id: 'pickers-community', + label: '@mui/x-date-pickers', + }, + { id: 'pickers-pro', label: '@mui/x-date-pickers-pro' }, + ], + }, + { + id: 'charts', + label: 'Charts', + children: [{ id: 'charts-community', label: '@mui/x-charts' }], + }, + { + id: 'tree-view', + label: 'Tree View', + children: [{ id: 'tree-view-community', label: '@mui/x-tree-view' }], + }, +]; + +export default function EditLeaves() { + const apiRef = useTreeViewApiRef(); + return ( + + + apiRef.current!.getItemOrderedChildrenIds(item.id).length === 0 + } + defaultExpandedItems={['grid', 'pickers']} + /> + + ); +} diff --git a/docs/data/tree-view/rich-tree-view/editing/EditLeaves.tsx.preview b/docs/data/tree-view/rich-tree-view/editing/EditLeaves.tsx.preview new file mode 100644 index 000000000000..e03aa45b1144 --- /dev/null +++ b/docs/data/tree-view/rich-tree-view/editing/EditLeaves.tsx.preview @@ -0,0 +1,9 @@ + + apiRef.current!.getItemOrderedChildrenIds(item.id).length === 0 + } + defaultExpandedItems={['grid', 'pickers']} +/> \ No newline at end of file diff --git a/docs/data/tree-view/rich-tree-view/editing/EditWithIcons.js b/docs/data/tree-view/rich-tree-view/editing/EditWithIcons.js new file mode 100644 index 000000000000..e9dd9dc73ab0 --- /dev/null +++ b/docs/data/tree-view/rich-tree-view/editing/EditWithIcons.js @@ -0,0 +1,117 @@ +import * as React from 'react'; +import Box from '@mui/material/Box'; +import IconButton from '@mui/material/IconButton'; +import EditOutlinedIcon from '@mui/icons-material/EditOutlined'; +import CloseRoundedIcon from '@mui/icons-material/CloseRounded'; +import CheckIcon from '@mui/icons-material/Check'; +import { RichTreeView } from '@mui/x-tree-view/RichTreeView'; +import { useTreeItem2Utils } from '@mui/x-tree-view/hooks'; +import { TreeItem2, TreeItem2Label } from '@mui/x-tree-view/TreeItem2'; +import { TreeItem2LabelInput } from '@mui/x-tree-view/TreeItem2LabelInput'; + +import { MUI_X_PRODUCTS } from './products'; + +function CustomLabel({ editing, editable, children, toggleItemEditing, ...other }) { + return ( + + {children} + {editable && ( + + + + )} + + ); +} + +function CustomLabelInput(props) { + const { handleCancelItemLabelEditing, handleSaveItemLabel, value, ...other } = + props; + + return ( + + + { + handleSaveItemLabel(event, value); + }} + > + + + + + + + ); +} + +const CustomTreeItem2 = React.forwardRef(function CustomTreeItem2(props, ref) { + const { interactions, status } = useTreeItem2Utils({ + itemId: props.itemId, + children: props.children, + }); + + const handleContentDoubleClick = (event) => { + event.defaultMuiPrevented = true; + }; + + const handleInputBlur = (event) => { + event.defaultMuiPrevented = true; + }; + + const handleInputKeyDown = (event) => { + event.defaultMuiPrevented = true; + }; + + return ( + + ); +}); + +export default function EditWithIcons() { + return ( + + + + ); +} diff --git a/docs/data/tree-view/rich-tree-view/editing/EditWithIcons.tsx b/docs/data/tree-view/rich-tree-view/editing/EditWithIcons.tsx new file mode 100644 index 000000000000..2a4f2521d7bb --- /dev/null +++ b/docs/data/tree-view/rich-tree-view/editing/EditWithIcons.tsx @@ -0,0 +1,149 @@ +import * as React from 'react'; +import Box from '@mui/material/Box'; +import IconButton from '@mui/material/IconButton'; +import EditOutlinedIcon from '@mui/icons-material/EditOutlined'; +import CloseRoundedIcon from '@mui/icons-material/CloseRounded'; +import CheckIcon from '@mui/icons-material/Check'; +import { RichTreeView } from '@mui/x-tree-view/RichTreeView'; +import { useTreeItem2Utils } from '@mui/x-tree-view/hooks'; +import { + TreeItem2, + TreeItem2Label, + TreeItem2Props, +} from '@mui/x-tree-view/TreeItem2'; +import { TreeItem2LabelInput } from '@mui/x-tree-view/TreeItem2LabelInput'; +import { + UseTreeItem2LabelInputSlotOwnProps, + UseTreeItem2LabelSlotOwnProps, +} from '@mui/x-tree-view/useTreeItem2'; +import { MUI_X_PRODUCTS } from './products'; + +interface CustomLabelProps extends UseTreeItem2LabelSlotOwnProps { + editable: boolean; + editing: boolean; + toggleItemEditing: () => void; +} + +function CustomLabel({ + editing, + editable, + children, + toggleItemEditing, + ...other +}: CustomLabelProps) { + return ( + + {children} + {editable && ( + + + + )} + + ); +} + +interface CustomLabelInputProps extends UseTreeItem2LabelInputSlotOwnProps { + handleCancelItemLabelEditing: (event: React.SyntheticEvent) => void; + handleSaveItemLabel: (event: React.SyntheticEvent, label: string) => void; + value: string; +} + +function CustomLabelInput(props: Omit) { + const { handleCancelItemLabelEditing, handleSaveItemLabel, value, ...other } = + props; + + return ( + + + { + handleSaveItemLabel(event, value); + }} + > + + + + + + + ); +} + +const CustomTreeItem2 = React.forwardRef(function CustomTreeItem2( + props: TreeItem2Props, + ref: React.Ref, +) { + const { interactions, status } = useTreeItem2Utils({ + itemId: props.itemId, + children: props.children, + }); + + const handleContentDoubleClick: UseTreeItem2LabelSlotOwnProps['onDoubleClick'] = ( + event, + ) => { + event.defaultMuiPrevented = true; + }; + + const handleInputBlur: UseTreeItem2LabelInputSlotOwnProps['onBlur'] = (event) => { + event.defaultMuiPrevented = true; + }; + + const handleInputKeyDown: UseTreeItem2LabelInputSlotOwnProps['onKeyDown'] = ( + event, + ) => { + event.defaultMuiPrevented = true; + }; + + return ( + + ); +}); + +export default function EditWithIcons() { + return ( + + + + ); +} diff --git a/docs/data/tree-view/rich-tree-view/editing/EditWithIcons.tsx.preview b/docs/data/tree-view/rich-tree-view/editing/EditWithIcons.tsx.preview new file mode 100644 index 000000000000..761e52103ecd --- /dev/null +++ b/docs/data/tree-view/rich-tree-view/editing/EditWithIcons.tsx.preview @@ -0,0 +1,8 @@ + \ No newline at end of file diff --git a/docs/data/tree-view/rich-tree-view/editing/EditingCallback.js b/docs/data/tree-view/rich-tree-view/editing/EditingCallback.js new file mode 100644 index 000000000000..11fce8f7f128 --- /dev/null +++ b/docs/data/tree-view/rich-tree-view/editing/EditingCallback.js @@ -0,0 +1,33 @@ +import * as React from 'react'; +import Box from '@mui/material/Box'; +import Stack from '@mui/material/Stack'; +import Typography from '@mui/material/Typography'; +import { RichTreeView } from '@mui/x-tree-view/RichTreeView'; +import { MUI_X_PRODUCTS } from './products'; + +export default function EditingCallback() { + const [lastEditedItem, setLastEditedItem] = React.useState(null); + + return ( + + {lastEditedItem ? ( + + The label of item with id {lastEditedItem.itemId} has been edited + to {lastEditedItem.label} + + ) : ( + No item has been edited yet + )} + + + setLastEditedItem({ itemId, label })} + /> + + + ); +} diff --git a/docs/data/tree-view/rich-tree-view/editing/EditingCallback.tsx b/docs/data/tree-view/rich-tree-view/editing/EditingCallback.tsx new file mode 100644 index 000000000000..772ae6e8c55d --- /dev/null +++ b/docs/data/tree-view/rich-tree-view/editing/EditingCallback.tsx @@ -0,0 +1,35 @@ +import * as React from 'react'; +import Box from '@mui/material/Box'; +import Stack from '@mui/material/Stack'; +import Typography from '@mui/material/Typography'; +import { RichTreeView } from '@mui/x-tree-view/RichTreeView'; +import { MUI_X_PRODUCTS } from './products'; + +export default function EditingCallback() { + const [lastEditedItem, setLastEditedItem] = React.useState<{ + itemId: string; + label: string; + } | null>(null); + + return ( + + {lastEditedItem ? ( + + The label of item with id {lastEditedItem!.itemId} has been edited + to {lastEditedItem!.label} + + ) : ( + No item has been edited yet + )} + + setLastEditedItem({ itemId, label })} + /> + + + ); +} diff --git a/docs/data/tree-view/rich-tree-view/editing/LabelEditingAllItems.js b/docs/data/tree-view/rich-tree-view/editing/LabelEditingAllItems.js new file mode 100644 index 000000000000..e4a7020a9fb5 --- /dev/null +++ b/docs/data/tree-view/rich-tree-view/editing/LabelEditingAllItems.js @@ -0,0 +1,17 @@ +import * as React from 'react'; +import Box from '@mui/material/Box'; +import { RichTreeView } from '@mui/x-tree-view/RichTreeView'; +import { MUI_X_PRODUCTS } from './products'; + +export default function LabelEditingAllItems() { + return ( + + + + ); +} diff --git a/docs/data/tree-view/rich-tree-view/editing/LabelEditingAllItems.tsx b/docs/data/tree-view/rich-tree-view/editing/LabelEditingAllItems.tsx new file mode 100644 index 000000000000..e4a7020a9fb5 --- /dev/null +++ b/docs/data/tree-view/rich-tree-view/editing/LabelEditingAllItems.tsx @@ -0,0 +1,17 @@ +import * as React from 'react'; +import Box from '@mui/material/Box'; +import { RichTreeView } from '@mui/x-tree-view/RichTreeView'; +import { MUI_X_PRODUCTS } from './products'; + +export default function LabelEditingAllItems() { + return ( + + + + ); +} diff --git a/docs/data/tree-view/rich-tree-view/editing/LabelEditingAllItems.tsx.preview b/docs/data/tree-view/rich-tree-view/editing/LabelEditingAllItems.tsx.preview new file mode 100644 index 000000000000..f6140084082a --- /dev/null +++ b/docs/data/tree-view/rich-tree-view/editing/LabelEditingAllItems.tsx.preview @@ -0,0 +1,6 @@ + \ No newline at end of file diff --git a/docs/data/tree-view/rich-tree-view/editing/LabelEditingSomeItems.js b/docs/data/tree-view/rich-tree-view/editing/LabelEditingSomeItems.js new file mode 100644 index 000000000000..a60407f22e05 --- /dev/null +++ b/docs/data/tree-view/rich-tree-view/editing/LabelEditingSomeItems.js @@ -0,0 +1,17 @@ +import * as React from 'react'; +import Box from '@mui/material/Box'; +import { RichTreeView } from '@mui/x-tree-view/RichTreeView'; +import { MUI_X_PRODUCTS } from './editableProducts'; + +export default function LabelEditingSomeItems() { + return ( + + Boolean(item?.editable)} + experimentalFeatures={{ labelEditing: true }} + defaultExpandedItems={['grid', 'pickers']} + /> + + ); +} diff --git a/docs/data/tree-view/rich-tree-view/editing/LabelEditingSomeItems.tsx b/docs/data/tree-view/rich-tree-view/editing/LabelEditingSomeItems.tsx new file mode 100644 index 000000000000..a60407f22e05 --- /dev/null +++ b/docs/data/tree-view/rich-tree-view/editing/LabelEditingSomeItems.tsx @@ -0,0 +1,17 @@ +import * as React from 'react'; +import Box from '@mui/material/Box'; +import { RichTreeView } from '@mui/x-tree-view/RichTreeView'; +import { MUI_X_PRODUCTS } from './editableProducts'; + +export default function LabelEditingSomeItems() { + return ( + + Boolean(item?.editable)} + experimentalFeatures={{ labelEditing: true }} + defaultExpandedItems={['grid', 'pickers']} + /> + + ); +} diff --git a/docs/data/tree-view/rich-tree-view/editing/LabelEditingSomeItems.tsx.preview b/docs/data/tree-view/rich-tree-view/editing/LabelEditingSomeItems.tsx.preview new file mode 100644 index 000000000000..bdd2489ac890 --- /dev/null +++ b/docs/data/tree-view/rich-tree-view/editing/LabelEditingSomeItems.tsx.preview @@ -0,0 +1,6 @@ + Boolean(item?.editable)} + experimentalFeatures={{ labelEditing: true }} + defaultExpandedItems={['grid', 'pickers']} +/> \ No newline at end of file diff --git a/docs/data/tree-view/rich-tree-view/editing/Validation.js b/docs/data/tree-view/rich-tree-view/editing/Validation.js new file mode 100644 index 000000000000..a851bf3090b5 --- /dev/null +++ b/docs/data/tree-view/rich-tree-view/editing/Validation.js @@ -0,0 +1,108 @@ +import * as React from 'react'; +import Box from '@mui/material/Box'; +import Tooltip from '@mui/material/Tooltip'; +import CheckCircleOutlineIcon from '@mui/icons-material/CheckCircleOutline'; +import ErrorOutlineIcon from '@mui/icons-material/ErrorOutline'; +import { RichTreeView } from '@mui/x-tree-view/RichTreeView'; +import { useTreeItem2Utils } from '@mui/x-tree-view/hooks'; +import { TreeItem2 } from '@mui/x-tree-view/TreeItem2'; + +import { TreeItem2LabelInput } from '@mui/x-tree-view/TreeItem2LabelInput'; +import { MUI_X_PRODUCTS } from './products'; + +const ERRORS = { + REQUIRED: 'The label cannot be empty', + INVALID: 'The label cannot contain digits', +}; + +function CustomLabelInput(props) { + const { error, ...other } = props; + + return ( + + + {error ? ( + + + + ) : ( + + + + )} + + ); +} + +const CustomTreeItem2 = React.forwardRef(function CustomTreeItem2(props, ref) { + const [error, setError] = React.useState(null); + const { interactions } = useTreeItem2Utils({ + itemId: props.itemId, + children: props.children, + }); + const validateLabel = (label) => { + if (!label) { + setError('REQUIRED'); + } else if (/\d/.test(label)) { + setError('INVALID'); + } else { + setError(null); + } + }; + + const handleInputBlur = (event) => { + if (error) { + event.defaultMuiPrevented = true; + } + }; + + const handleInputKeyDown = (event) => { + event.defaultMuiPrevented = true; + const target = event.target; + + if (event.key === 'Enter' && target.value) { + if (error) { + return; + } + setError(null); + interactions.handleSaveItemLabel(event, target.value); + } else if (event.key === 'Escape') { + setError(null); + interactions.handleCancelItemLabelEditing(event); + } + }; + + const handleInputChange = (event) => { + validateLabel(event.target.value); + }; + + return ( + + ); +}); + +export default function Validation() { + return ( + + + + ); +} diff --git a/docs/data/tree-view/rich-tree-view/editing/Validation.tsx b/docs/data/tree-view/rich-tree-view/editing/Validation.tsx new file mode 100644 index 000000000000..450a0b3711dc --- /dev/null +++ b/docs/data/tree-view/rich-tree-view/editing/Validation.tsx @@ -0,0 +1,117 @@ +import * as React from 'react'; +import Box from '@mui/material/Box'; +import Tooltip from '@mui/material/Tooltip'; +import CheckCircleOutlineIcon from '@mui/icons-material/CheckCircleOutline'; +import ErrorOutlineIcon from '@mui/icons-material/ErrorOutline'; +import { RichTreeView } from '@mui/x-tree-view/RichTreeView'; +import { useTreeItem2Utils } from '@mui/x-tree-view/hooks'; +import { TreeItem2, TreeItem2Props } from '@mui/x-tree-view/TreeItem2'; +import { UseTreeItem2LabelInputSlotOwnProps } from '@mui/x-tree-view/useTreeItem2'; +import { TreeItem2LabelInput } from '@mui/x-tree-view/TreeItem2LabelInput'; +import { MUI_X_PRODUCTS } from './products'; + +const ERRORS = { + REQUIRED: 'The label cannot be empty', + INVALID: 'The label cannot contain digits', +}; + +interface CustomLabelInputProps extends UseTreeItem2LabelInputSlotOwnProps { + error: null | keyof typeof ERRORS; +} + +function CustomLabelInput(props: Omit) { + const { error, ...other } = props; + + return ( + + + {error ? ( + + + + ) : ( + + + + )} + + ); +} + +const CustomTreeItem2 = React.forwardRef(function CustomTreeItem2( + props: TreeItem2Props, + ref: React.Ref, +) { + const [error, setError] = React.useState(null); + const { interactions } = useTreeItem2Utils({ + itemId: props.itemId, + children: props.children, + }); + const validateLabel = (label: string) => { + if (!label) { + setError('REQUIRED'); + } else if (/\d/.test(label)) { + setError('INVALID'); + } else { + setError(null); + } + }; + + const handleInputBlur: UseTreeItem2LabelInputSlotOwnProps['onBlur'] = (event) => { + if (error) { + event.defaultMuiPrevented = true; + } + }; + + const handleInputKeyDown: UseTreeItem2LabelInputSlotOwnProps['onKeyDown'] = ( + event, + ) => { + event.defaultMuiPrevented = true; + const target = event.target as HTMLInputElement; + + if (event.key === 'Enter' && target.value) { + if (error) { + return; + } + setError(null); + interactions.handleSaveItemLabel(event, target.value); + } else if (event.key === 'Escape') { + setError(null); + interactions.handleCancelItemLabelEditing(event); + } + }; + + const handleInputChange = (event: React.ChangeEvent) => { + validateLabel(event.target.value); + }; + + return ( + + ); +}); + +export default function Validation() { + return ( + + + + ); +} diff --git a/docs/data/tree-view/rich-tree-view/editing/Validation.tsx.preview b/docs/data/tree-view/rich-tree-view/editing/Validation.tsx.preview new file mode 100644 index 000000000000..6657b0a9a6a8 --- /dev/null +++ b/docs/data/tree-view/rich-tree-view/editing/Validation.tsx.preview @@ -0,0 +1,7 @@ + \ No newline at end of file diff --git a/docs/data/tree-view/rich-tree-view/editing/editableProducts.ts b/docs/data/tree-view/rich-tree-view/editing/editableProducts.ts new file mode 100644 index 000000000000..ca7945031fb5 --- /dev/null +++ b/docs/data/tree-view/rich-tree-view/editing/editableProducts.ts @@ -0,0 +1,43 @@ +import { TreeViewBaseItem } from '@mui/x-tree-view/models'; + +type Editable = { + editable?: boolean; + id: string; + label: string; +}; + +export const MUI_X_PRODUCTS: TreeViewBaseItem[] = [ + { + id: 'grid', + label: 'Data Grid', + + children: [ + { id: 'grid-community', label: '@mui/x-data-grid editable', editable: true }, + { id: 'grid-pro', label: '@mui/x-data-grid-pro editable', editable: true }, + { id: 'grid-premium', label: '@mui/x-data-grid-premium' }, + ], + }, + { + id: 'pickers', + label: 'Date and time pickers', + + children: [ + { + id: 'pickers-community', + label: '@mui/x-date-pickers', + }, + { id: 'pickers-pro', label: '@mui/x-date-pickers-pro' }, + ], + }, + { + id: 'charts', + label: 'Charts', + + children: [{ id: 'charts-community', label: '@mui/x-charts' }], + }, + { + id: 'tree-view', + label: 'Tree View', + children: [{ id: 'tree-view-community', label: '@mui/x-tree-view' }], + }, +]; diff --git a/docs/data/tree-view/rich-tree-view/editing/editing.md b/docs/data/tree-view/rich-tree-view/editing/editing.md new file mode 100644 index 000000000000..73e923bb7cff --- /dev/null +++ b/docs/data/tree-view/rich-tree-view/editing/editing.md @@ -0,0 +1,99 @@ +--- +productId: x-tree-view +title: Rich Tree View - Editing +githubLabel: 'component: tree view' +packageName: '@mui/x-tree-view' +waiAria: https://www.w3.org/WAI/ARIA/apg/patterns/treeview/ +packageName: '@mui/x-tree-view' +--- + +# Rich Tree View - Label editing + +

Learn how to edit the label of Tree View items.

+ +## Enable label editing + +You can use the `isItemEditable` prop to enable editing. +If set to `true`, this prop will enable label editing on all items: + +{{"demo": "LabelEditingAllItems.js"}} + +:::success +If an item is editable, the editing state can be toggled by double clicking on it, or by pressing Enter on the keyboard when the item is in focus. + +Once an item is in editing state, the value of the label can be edited. Pressing Enter again or bluring the item will save the new value. Pressing Esc will cancel the action and restore the item to its original state. + +::: + +## Limit editing to some items + +If you pass a method to `isItemEditable`, only the items for which the method returns `true` will be editable: + +{{"demo": "LabelEditingSomeItems.js"}} + +### Limit editing to leaves + +You can limit the editing to just the leaves of the tree. + +{{"demo": "EditLeaves.js"}} + +## Track item label change + +Use the `onItemLabelChange` prop to trigger an action when the label of an item changes. + +{{"demo": "EditingCallback.js"}} + +## Change the default behavior + +By default, blurring the tree item saves the new value if there is one. +To modify this behavior, use the `slotProps` of the `TreeItem2`. + +{{"demo": "CustomBehavior.js"}} + +## Validation + +You can override the event handlers of the `labelInput` and implement a custom validation logic using the interaction methods from `useTreeItem2Utils`. + +{{"demo": "Validation.js"}} + +## Enable editing using only icons + +The demo below shows how to entirely override the editing behavior, and implement it using icons. + +{{"demo": "EditWithIcons.js"}} + +## Create a custom labelInput + +The demo below shows how to use a different component in the `labelInput` slot. + +{{"demo": "CustomLabelInput.js"}} + +## Imperative API + +:::success +To use the `apiRef` object, you need to initialize it using the `useTreeViewApiRef` hook as follows: + +```tsx +const apiRef = useTreeViewApiRef(); + +return ; +``` + +When your component first renders, `apiRef` will be `undefined`. +After this initial render, `apiRef` holds methods to interact imperatively with the Tree View. +::: + +### Change the label of an item + +Use the `setItemExpansion` API method to change the expansion of an item. + +```ts +apiRef.current.updateItemLabel( + // The id of the item to to update + itemId, + // The new label of the item. + newLabel, +); +``` + +{{"demo": "ApiMethodUpdateItemLabel.js"}} diff --git a/docs/data/tree-view/rich-tree-view/editing/employees.ts b/docs/data/tree-view/rich-tree-view/editing/employees.ts new file mode 100644 index 000000000000..5e1e16f8a079 --- /dev/null +++ b/docs/data/tree-view/rich-tree-view/editing/employees.ts @@ -0,0 +1,37 @@ +import { TreeViewBaseItem } from '@mui/x-tree-view/models'; + +export type Employee = { + editable?: boolean; + id: string; + firstName: string; + lastName: string; +}; + +export const EMPLOYEES: TreeViewBaseItem[] = [ + { + id: '1', + firstName: 'Jane', + lastName: 'Doe', + editable: true, + children: [ + { id: '1.1', firstName: 'Elena', lastName: 'Kim', editable: true }, + { id: '1.2', firstName: 'Noah', lastName: 'Rodriguez', editable: true }, + { id: '1.3', firstName: 'Maya', lastName: 'Patel', editable: true }, + ], + }, + { + id: '2', + firstName: 'Liam', + lastName: 'Clarke', + editable: true, + children: [ + { + id: '2.1', + firstName: 'Ethan', + lastName: 'Lee', + editable: true, + }, + { id: '2.2', firstName: 'Ava', lastName: 'Jones', editable: true }, + ], + }, +]; diff --git a/docs/data/tree-view/rich-tree-view/editing/products.ts b/docs/data/tree-view/rich-tree-view/editing/products.ts new file mode 100644 index 000000000000..97929673353e --- /dev/null +++ b/docs/data/tree-view/rich-tree-view/editing/products.ts @@ -0,0 +1,37 @@ +import { TreeViewBaseItem } from '@mui/x-tree-view/models'; + +export const MUI_X_PRODUCTS: TreeViewBaseItem[] = [ + { + id: 'grid', + label: 'Data Grid', + + children: [ + { id: 'grid-community', label: '@mui/x-data-grid' }, + { id: 'grid-pro', label: '@mui/x-data-grid-pro' }, + { id: 'grid-premium', label: '@mui/x-data-grid-premium' }, + ], + }, + { + id: 'pickers', + label: 'Date and time pickers', + + children: [ + { + id: 'pickers-community', + label: '@mui/x-date-pickers', + }, + { id: 'pickers-pro', label: '@mui/x-date-pickers-pro' }, + ], + }, + { + id: 'charts', + label: 'Charts', + + children: [{ id: 'charts-community', label: '@mui/x-charts' }], + }, + { + id: 'tree-view', + label: 'Tree View', + children: [{ id: 'tree-view-community', label: '@mui/x-tree-view' }], + }, +]; diff --git a/docs/pages/x/api/charts/line-series-type.json b/docs/pages/x/api/charts/line-series-type.json index 8043690cf449..61608bfce9c6 100644 --- a/docs/pages/x/api/charts/line-series-type.json +++ b/docs/pages/x/api/charts/line-series-type.json @@ -4,6 +4,7 @@ "properties": { "type": { "type": { "description": "'line'" }, "required": true }, "area": { "type": { "description": "boolean" } }, + "baseline": { "type": { "description": "number | 'min' | 'max'" }, "default": "0" }, "color": { "type": { "description": "string" } }, "connectNulls": { "type": { "description": "boolean" }, "default": "false" }, "curve": { "type": { "description": "CurveType" } }, diff --git a/docs/pages/x/api/tree-view/rich-tree-view-pro.json b/docs/pages/x/api/tree-view/rich-tree-view-pro.json index 03bea300243d..7cd100636eb8 100644 --- a/docs/pages/x/api/tree-view/rich-tree-view-pro.json +++ b/docs/pages/x/api/tree-view/rich-tree-view-pro.json @@ -3,7 +3,7 @@ "apiRef": { "type": { "name": "shape", - "description": "{ current?: { focusItem: func, getItem: func, getItemDOMElement: func, getItemOrderedChildrenIds: func, getItemTree: func, selectItem: func, setItemExpansion: func } }" + "description": "{ current?: { focusItem: func, getItem: func, getItemDOMElement: func, getItemOrderedChildrenIds: func, getItemTree: func, selectItem: func, setItemExpansion: func, updateItemLabel: func } }" } }, "canMoveItemToNewPosition": { @@ -31,7 +31,7 @@ "experimentalFeatures": { "type": { "name": "shape", - "description": "{ indentationAtItemLevel?: bool, itemsReordering?: bool }" + "description": "{ indentationAtItemLevel?: bool, itemsReordering?: bool, labelEditing?: bool }" } }, "getItemId": { @@ -61,6 +61,7 @@ "returned": "boolean" } }, + "isItemEditable": { "type": { "name": "union", "description": "func
| bool" } }, "isItemReorderable": { "type": { "name": "func" }, "default": "() => true", @@ -104,6 +105,13 @@ "describedArgs": ["event", "itemId"] } }, + "onItemLabelChange": { + "type": { "name": "func" }, + "signature": { + "type": "function(itemId: TreeViewItemId, newLabel: string) => void", + "describedArgs": ["itemId", "newLabel"] + } + }, "onItemPositionChange": { "type": { "name": "func" }, "signature": { diff --git a/docs/pages/x/api/tree-view/rich-tree-view.json b/docs/pages/x/api/tree-view/rich-tree-view.json index 7857c5c370ba..53335f98fc5e 100644 --- a/docs/pages/x/api/tree-view/rich-tree-view.json +++ b/docs/pages/x/api/tree-view/rich-tree-view.json @@ -3,7 +3,7 @@ "apiRef": { "type": { "name": "shape", - "description": "{ current?: { focusItem: func, getItem: func, getItemDOMElement: func, getItemOrderedChildrenIds: func, getItemTree: func, selectItem: func, setItemExpansion: func } }" + "description": "{ current?: { focusItem: func, getItem: func, getItemDOMElement: func, getItemOrderedChildrenIds: func, getItemTree: func, selectItem: func, setItemExpansion: func, updateItemLabel: func } }" } }, "checkboxSelection": { "type": { "name": "bool" }, "default": "false" }, @@ -21,7 +21,10 @@ "default": "'content'" }, "experimentalFeatures": { - "type": { "name": "shape", "description": "{ indentationAtItemLevel?: bool }" } + "type": { + "name": "shape", + "description": "{ indentationAtItemLevel?: bool, labelEditing?: bool }" + } }, "getItemId": { "type": { "name": "func" }, @@ -50,6 +53,7 @@ "returned": "boolean" } }, + "isItemEditable": { "type": { "name": "union", "description": "func
| bool" } }, "itemChildrenIndentation": { "type": { "name": "union", "description": "number
| string" }, "default": "12px" @@ -83,6 +87,13 @@ "describedArgs": ["event", "itemId"] } }, + "onItemLabelChange": { + "type": { "name": "func" }, + "signature": { + "type": "function(itemId: TreeViewItemId, newLabel: string) => void", + "describedArgs": ["itemId", "newLabel"] + } + }, "onItemSelectionToggle": { "type": { "name": "func" }, "signature": { diff --git a/docs/pages/x/api/tree-view/tree-item-2.json b/docs/pages/x/api/tree-view/tree-item-2.json index 64dd97521004..547383a665af 100644 --- a/docs/pages/x/api/tree-view/tree-item-2.json +++ b/docs/pages/x/api/tree-view/tree-item-2.json @@ -59,6 +59,12 @@ "default": "TreeItem2Label", "class": "MuiTreeItem2-label" }, + { + "name": "labelInput", + "description": "The component that renders the input to edit the label when the item is editable and is currently being edited.", + "default": "TreeItem2LabelInput", + "class": "MuiTreeItem2-labelInput" + }, { "name": "dragAndDropOverlay", "description": "The component that renders the overlay when an item reordering is ongoing.\nWarning: This slot is only useful when using the `RichTreeViewPro` component.", @@ -81,6 +87,18 @@ "description": "State class applied to the element when disabled.", "isGlobal": true }, + { + "key": "editable", + "className": "MuiTreeItem2-editable", + "description": "Styles applied to the content of the items that are editable.", + "isGlobal": false + }, + { + "key": "editing", + "className": "MuiTreeItem2-editing", + "description": "Styles applied to the content element when editing is enabled.", + "isGlobal": false + }, { "key": "expanded", "className": "Mui-expanded", diff --git a/docs/pages/x/api/tree-view/tree-item.json b/docs/pages/x/api/tree-view/tree-item.json index 2a7c1b9c0032..7873eb960165 100644 --- a/docs/pages/x/api/tree-view/tree-item.json +++ b/docs/pages/x/api/tree-view/tree-item.json @@ -73,6 +73,18 @@ "description": "Styles applied to the drag and drop overlay.", "isGlobal": false }, + { + "key": "editable", + "className": "MuiTreeItem-editable", + "description": "Styles applied to the content of the items that are editable.", + "isGlobal": false + }, + { + "key": "editing", + "className": "MuiTreeItem-editing", + "description": "Styles applied to the content element when editing is enabled.", + "isGlobal": false + }, { "key": "expanded", "className": "Mui-expanded", @@ -97,6 +109,12 @@ "description": "Styles applied to the label element.", "isGlobal": false }, + { + "key": "labelInput", + "className": "MuiTreeItem-labelInput", + "description": "Styles applied to the input element that is visible when editing is enabled.", + "isGlobal": false + }, { "key": "root", "className": "MuiTreeItem-root", diff --git a/docs/pages/x/react-tree-view/rich-tree-view/editing.js b/docs/pages/x/react-tree-view/rich-tree-view/editing.js new file mode 100644 index 000000000000..73aff62cb36f --- /dev/null +++ b/docs/pages/x/react-tree-view/rich-tree-view/editing.js @@ -0,0 +1,7 @@ +import * as React from 'react'; +import MarkdownDocs from 'docs/src/modules/components/MarkdownDocs'; +import * as pageProps from 'docsx/data/tree-view/rich-tree-view/editing/editing.md?muiMarkdown'; + +export default function Page() { + return ; +} diff --git a/docs/translations/api-docs/charts/line-series-type.json b/docs/translations/api-docs/charts/line-series-type.json index ff576209272d..824efbcd4acb 100644 --- a/docs/translations/api-docs/charts/line-series-type.json +++ b/docs/translations/api-docs/charts/line-series-type.json @@ -3,6 +3,9 @@ "propertiesDescriptions": { "type": { "description": "" }, "area": { "description": "" }, + "baseline": { + "description": "

The value of the line at the base of the series area.

- 'min' the area will fill the space under the line.
- 'max' the area will fill the space above the line.
- number the area will fill the space between this value and the line

\n" + }, "color": { "description": "" }, "connectNulls": { "description": "If true, line and area connect points separated by null values." diff --git a/docs/translations/api-docs/tree-view/rich-tree-view-pro/rich-tree-view-pro.json b/docs/translations/api-docs/tree-view/rich-tree-view-pro/rich-tree-view-pro.json index bcd6e7bdb905..3a33b96c08bd 100644 --- a/docs/translations/api-docs/tree-view/rich-tree-view-pro/rich-tree-view-pro.json +++ b/docs/translations/api-docs/tree-view/rich-tree-view-pro/rich-tree-view-pro.json @@ -8,7 +8,7 @@ "description": "Used to determine if a given item can move to some new position.", "typeDescriptions": { "params": "The params describing the item re-ordering.", - "params.itemId": "The id of the item to check.", + "params.itemId": "The id of the item that is being moved to a new position.", "params.oldPosition": "The old position of the item.", "params.newPosition": "The new position of the item.", "boolean": "true if the item can move to the new position." @@ -55,6 +55,9 @@ "boolean": "true if the item should be disabled." } }, + "isItemEditable": { + "description": "Determines if a given item is editable or not. Make sure to also enable the labelEditing experimental feature: <RichTreeViewPro experimentalFeatures={{ labelEditing: true }} />. By default, the items are not editable." + }, "isItemReorderable": { "description": "Used to determine if a given item can be reordered.", "typeDescriptions": { @@ -100,6 +103,13 @@ "itemId": "The id of the focused item." } }, + "onItemLabelChange": { + "description": "Callback fired when the label of an item changes.", + "typeDescriptions": { + "itemId": "The id of the item that was edited.", + "newLabel": "The new label of the items." + } + }, "onItemPositionChange": { "description": "Callback fired when a tree item is moved in the tree.", "typeDescriptions": { diff --git a/docs/translations/api-docs/tree-view/rich-tree-view/rich-tree-view.json b/docs/translations/api-docs/tree-view/rich-tree-view/rich-tree-view.json index 3b94a5f52442..3ba5993e5187 100644 --- a/docs/translations/api-docs/tree-view/rich-tree-view/rich-tree-view.json +++ b/docs/translations/api-docs/tree-view/rich-tree-view/rich-tree-view.json @@ -45,6 +45,9 @@ "boolean": "true if the item should be disabled." } }, + "isItemEditable": { + "description": "Determines if a given item is editable or not. Make sure to also enable the labelEditing experimental feature: <RichTreeViewPro experimentalFeatures={{ labelEditing: true }} />. By default, the items are not editable." + }, "itemChildrenIndentation": { "description": "Horizontal indentation between an item and its children. Examples: 24, "24px", "2rem", "2em"." }, @@ -80,6 +83,13 @@ "itemId": "The id of the focused item." } }, + "onItemLabelChange": { + "description": "Callback fired when the label of an item changes.", + "typeDescriptions": { + "itemId": "The id of the item that was edited.", + "newLabel": "The new label of the items." + } + }, "onItemSelectionToggle": { "description": "Callback fired when a tree item is selected or deselected.", "typeDescriptions": { diff --git a/docs/translations/api-docs/tree-view/tree-item-2/tree-item-2.json b/docs/translations/api-docs/tree-view/tree-item-2/tree-item-2.json index 6a5f8d2ae832..1ec2e89e98d8 100644 --- a/docs/translations/api-docs/tree-view/tree-item-2/tree-item-2.json +++ b/docs/translations/api-docs/tree-view/tree-item-2/tree-item-2.json @@ -23,6 +23,15 @@ "nodeName": "the element", "conditions": "disabled" }, + "editable": { + "description": "Styles applied to {{nodeName}}.", + "nodeName": "the content of the items that are editable" + }, + "editing": { + "description": "Styles applied to {{nodeName}} when {{conditions}}.", + "nodeName": "the content element", + "conditions": "editing is enabled" + }, "expanded": { "description": "State class applied to {{nodeName}} when {{conditions}}.", "nodeName": "the content element", @@ -50,6 +59,7 @@ "icon": "The icon to display next to the tree item's label.", "iconContainer": "The component that renders the icon.", "label": "The component that renders the item label.", + "labelInput": "The component that renders the input to edit the label when the item is editable and is currently being edited.", "root": "The component that renders the root." } } diff --git a/docs/translations/api-docs/tree-view/tree-item/tree-item.json b/docs/translations/api-docs/tree-view/tree-item/tree-item.json index 596c008d70f1..f0febee14167 100644 --- a/docs/translations/api-docs/tree-view/tree-item/tree-item.json +++ b/docs/translations/api-docs/tree-view/tree-item/tree-item.json @@ -41,6 +41,15 @@ "description": "Styles applied to {{nodeName}}.", "nodeName": "the drag and drop overlay" }, + "editable": { + "description": "Styles applied to {{nodeName}}.", + "nodeName": "the content of the items that are editable" + }, + "editing": { + "description": "Styles applied to {{nodeName}} when {{conditions}}.", + "nodeName": "the content element", + "conditions": "editing is enabled" + }, "expanded": { "description": "State class applied to {{nodeName}} when {{conditions}}.", "nodeName": "the content element", @@ -56,6 +65,11 @@ "nodeName": "the tree item icon" }, "label": { "description": "Styles applied to {{nodeName}}.", "nodeName": "the label element" }, + "labelInput": { + "description": "Styles applied to {{nodeName}} when {{conditions}}.", + "nodeName": "the input element that is visible", + "conditions": "editing is enabled" + }, "root": { "description": "Styles applied to the root element." }, "selected": { "description": "State class applied to {{nodeName}} when {{conditions}}.", diff --git a/netlify.toml b/netlify.toml index 25bf422e747a..4d663716bd27 100644 --- a/netlify.toml +++ b/netlify.toml @@ -10,7 +10,6 @@ [build.environment] NODE_VERSION = "18" NODE_OPTIONS = "--max_old_space_size=4096" - PNPM_FLAGS = "--shamefully-hoist" [[plugins]] package = "./packages/netlify-plugin-cache-docs" diff --git a/packages/x-charts/src/LineChart/AreaPlot.tsx b/packages/x-charts/src/LineChart/AreaPlot.tsx index 79412e5211e6..01956e3b847b 100644 --- a/packages/x-charts/src/LineChart/AreaPlot.tsx +++ b/packages/x-charts/src/LineChart/AreaPlot.tsx @@ -59,6 +59,7 @@ const useAggregatedData = () => { stackedData, data, connectNulls, + baseline, } = series[seriesId]; const xAxisId = xAxisIdProp ?? xAxisKey; @@ -97,6 +98,16 @@ const useAggregatedData = () => { .x((d) => xScale(d.x)) .defined((_, i) => connectNulls || data[i] != null) .y0((d) => { + if (typeof baseline === 'number') { + return yScale(baseline)!; + } + if (baseline === 'max') { + return yScale.range()[1]; + } + if (baseline === 'min') { + return yScale.range()[0]; + } + const value = d.y && yScale(d.y[0])!; if (Number.isNaN(value)) { return yScale.range()[0]; diff --git a/packages/x-charts/src/LineChart/extremums.ts b/packages/x-charts/src/LineChart/extremums.ts index e913bc31c26b..e0fdcfac51c0 100644 --- a/packages/x-charts/src/LineChart/extremums.ts +++ b/packages/x-charts/src/LineChart/extremums.ts @@ -43,8 +43,11 @@ export const getExtremumY: ExtremumGetter<'line'> = (params) => { const { area, stackedData } = series[seriesId]; const isArea = area !== undefined; + // Since this series is not used to display an area, we do not consider the base (the d[0]). const getValues: GetValuesTypes = - isArea && axis.scaleType !== 'log' ? (d) => d : (d) => [d[1], d[1]]; // Since this series is not used to display an area, we do not consider the base (the d[0]). + isArea && axis.scaleType !== 'log' && typeof series[seriesId].baseline !== 'string' + ? (d) => d + : (d) => [d[1], d[1]]; const seriesExtremums = getSeriesExtremums(getValues, stackedData); diff --git a/packages/x-charts/src/models/seriesType/line.ts b/packages/x-charts/src/models/seriesType/line.ts index 5e4a6f481da9..744749d66833 100644 --- a/packages/x-charts/src/models/seriesType/line.ts +++ b/packages/x-charts/src/models/seriesType/line.ts @@ -82,6 +82,16 @@ export interface LineSeriesType * @default 'none' */ stackOffset?: StackOffsetType; + /** + * The value of the line at the base of the series area. + * + * - `'min'` the area will fill the space **under** the line. + * - `'max'` the area will fill the space **above** the line. + * - `number` the area will fill the space between this value and the line + * + * @default 0 + */ + baseline?: number | 'min' | 'max'; } /** diff --git a/packages/x-date-pickers/src/DateCalendar/tests/DateCalendar.test.tsx b/packages/x-date-pickers/src/DateCalendar/tests/DateCalendar.test.tsx index 5d4ff9b31eed..fb52644ced5b 100644 --- a/packages/x-date-pickers/src/DateCalendar/tests/DateCalendar.test.tsx +++ b/packages/x-date-pickers/src/DateCalendar/tests/DateCalendar.test.tsx @@ -247,7 +247,9 @@ describe('', () => { userEvent.mousePress(screen.getByRole('gridcell', { name: '2' })); expect(onChange.callCount).to.equal(1); - expect(onChange.lastCall.firstArg).toEqualDateTime(adapterToUse.date('2018-01-02T11:11:11')); + expect(onChange.lastCall.firstArg).toEqualDateTime( + adapterToUse.date('2018-01-02T11:11:11.111'), + ); }); it('should complete weeks when showDaysOutsideCurrentMonth=true', () => { diff --git a/packages/x-date-pickers/src/internals/utils/date-utils.test.ts b/packages/x-date-pickers/src/internals/utils/date-utils.test.ts index 6f7e2c855e30..a9d642959c4e 100644 --- a/packages/x-date-pickers/src/internals/utils/date-utils.test.ts +++ b/packages/x-date-pickers/src/internals/utils/date-utils.test.ts @@ -32,9 +32,9 @@ describe('findClosestEnabledDate', () => { disableFuture: false, disablePast: false, timezone: 'default', - })!; + }); - expect(adapterToUse.isSameDay(result, adapterToUse.date('2000-01-01'))).to.equal(true); + expect(result).toEqualDateTime(adapterToUse.date('2000-01-01')); }); it('should return next 18th going from 10th', () => { @@ -47,9 +47,9 @@ describe('findClosestEnabledDate', () => { disableFuture: false, disablePast: false, timezone: 'default', - })!; + }); - expect(adapterToUse.isSameDay(result, adapterToUse.date('2018-08-18'))).to.equal(true); + expect(result).toEqualDateTime(adapterToUse.date('2018-08-18')); }); it('should return previous 18th going from 1st', () => { @@ -62,9 +62,9 @@ describe('findClosestEnabledDate', () => { disableFuture: false, disablePast: false, timezone: 'default', - })!; + }); - expect(adapterToUse.isSameDay(result, adapterToUse.date('2018-07-18'))).to.equal(true); + expect(result).toEqualDateTime(adapterToUse.date('2018-07-18')); }); it('should return future 18th if disablePast', () => { @@ -78,7 +78,7 @@ describe('findClosestEnabledDate', () => { disableFuture: false, disablePast: true, timezone: 'default', - })!; + }); expect(adapterToUse.isBefore(result, today)).to.equal(false); expect(adapterToUse.isBefore(result, adapterToUse.addDays(today, 31))).to.equal(true); @@ -95,9 +95,9 @@ describe('findClosestEnabledDate', () => { disableFuture: true, disablePast: true, timezone: 'default', - })!; + }); - expect(adapterToUse.isSameDay(result, today)).to.equal(true); + expect(result).toEqualDateTime(today); }); it('should return now with given time part if disablePast and now is valid', () => { @@ -113,13 +113,13 @@ describe('findClosestEnabledDate', () => { disableFuture: false, disablePast: true, timezone: 'default', - })!; + }); expect(result).toEqualDateTime(adapterToUse.addDays(tryDate, 1)); clock.reset(); }); - it('should fallback to today if disablePast+disableFuture and now is invalid', () => { + it('should return `null` when disablePast+disableFuture and now is invalid', () => { const today = adapterToUse.date(); const result = findClosestEnabledDate({ date: adapterToUse.date('2000-01-01'), @@ -132,7 +132,7 @@ describe('findClosestEnabledDate', () => { timezone: 'default', }); - expect(adapterToUse.isEqual(result, adapterToUse.date())); + expect(result).to.equal(null); }); it('should return minDate if it is after the date and valid', () => { @@ -145,9 +145,9 @@ describe('findClosestEnabledDate', () => { disableFuture: false, disablePast: false, timezone: 'default', - })!; + }); - expect(adapterToUse.isSameDay(result, adapterToUse.date('2018-08-18'))).to.equal(true); + expect(result).toEqualDateTime(adapterToUse.date('2018-08-18')); }); it('should return next 18th after minDate', () => { @@ -160,9 +160,27 @@ describe('findClosestEnabledDate', () => { disableFuture: false, disablePast: false, timezone: 'default', - })!; + }); + + expect(result).toEqualDateTime(adapterToUse.date('2018-08-18')); + }); + + it('should keep the time of the `date` when `disablePast`', () => { + const clock = useFakeTimers({ now: new Date('2000-01-02T11:12:13.123Z') }); + + const result = findClosestEnabledDate({ + date: adapterToUse.date('2000-01-01T11:12:13.550Z'), + minDate: adapterToUse.date('1900-01-01'), + maxDate: adapterToUse.date('2100-01-01'), + utils: adapterToUse, + isDateDisabled: () => false, + disableFuture: false, + disablePast: true, + timezone: 'default', + }); - expect(adapterToUse.isSameDay(result, adapterToUse.date('2018-08-18'))).to.equal(true); + expect(result).toEqualDateTime(adapterToUse.date('2000-01-02T11:12:13.550Z')); + clock.reset(); }); it('should return maxDate if it is before the date and valid', () => { @@ -175,9 +193,9 @@ describe('findClosestEnabledDate', () => { disableFuture: false, disablePast: false, timezone: 'default', - })!; + }); - expect(adapterToUse.isSameDay(result, adapterToUse.date('2018-07-18'))).to.equal(true); + expect(result).toEqualDateTime(adapterToUse.date('2018-07-18')); }); it('should return previous 18th before maxDate', () => { @@ -190,9 +208,9 @@ describe('findClosestEnabledDate', () => { disableFuture: false, disablePast: false, timezone: 'default', - })!; + }); - expect(adapterToUse.isSameDay(result, adapterToUse.date('2018-07-18'))).to.equal(true); + expect(result).toEqualDateTime(adapterToUse.date('2018-07-18')); }); it('should return null if minDate is after maxDate', () => { @@ -205,7 +223,7 @@ describe('findClosestEnabledDate', () => { disableFuture: false, disablePast: false, timezone: 'default', - })!; + }); expect(result).to.equal(null); }); diff --git a/packages/x-date-pickers/src/internals/utils/date-utils.ts b/packages/x-date-pickers/src/internals/utils/date-utils.ts index 5f04069743a4..781ca434fba6 100644 --- a/packages/x-date-pickers/src/internals/utils/date-utils.ts +++ b/packages/x-date-pickers/src/internals/utils/date-utils.ts @@ -17,6 +17,7 @@ export const mergeDateAndTime = ( mergedDate = utils.setHours(mergedDate, utils.getHours(timeParam)); mergedDate = utils.setMinutes(mergedDate, utils.getMinutes(timeParam)); mergedDate = utils.setSeconds(mergedDate, utils.getSeconds(timeParam)); + mergedDate = utils.setMilliseconds(mergedDate, utils.getMilliseconds(timeParam)); return mergedDate; }; diff --git a/packages/x-tree-view-pro/src/RichTreeViewPro/RichTreeViewPro.plugins.ts b/packages/x-tree-view-pro/src/RichTreeViewPro/RichTreeViewPro.plugins.ts index 70dbf423afe4..7e7adee15189 100644 --- a/packages/x-tree-view-pro/src/RichTreeViewPro/RichTreeViewPro.plugins.ts +++ b/packages/x-tree-view-pro/src/RichTreeViewPro/RichTreeViewPro.plugins.ts @@ -13,6 +13,8 @@ import { ConvertPluginsIntoSignatures, MergeSignaturesProperty, TreeViewCorePluginParameters, + useTreeViewLabel, + UseTreeViewLabelParameters, } from '@mui/x-tree-view/internals'; import { useTreeViewItemsReordering, @@ -26,6 +28,7 @@ export const RICH_TREE_VIEW_PRO_PLUGINS = [ useTreeViewFocus, useTreeViewKeyboardNavigation, useTreeViewIcons, + useTreeViewLabel, useTreeViewItemsReordering, ] as const; @@ -51,4 +54,5 @@ export interface RichTreeViewProPluginParameters, UseTreeViewIconsParameters, + UseTreeViewLabelParameters, UseTreeViewItemsReorderingParameters {} diff --git a/packages/x-tree-view-pro/src/RichTreeViewPro/RichTreeViewPro.tsx b/packages/x-tree-view-pro/src/RichTreeViewPro/RichTreeViewPro.tsx index e71cc8ed6534..c08210e09782 100644 --- a/packages/x-tree-view-pro/src/RichTreeViewPro/RichTreeViewPro.tsx +++ b/packages/x-tree-view-pro/src/RichTreeViewPro/RichTreeViewPro.tsx @@ -190,12 +190,13 @@ RichTreeViewPro.propTypes = { getItemTree: PropTypes.func.isRequired, selectItem: PropTypes.func.isRequired, setItemExpansion: PropTypes.func.isRequired, + updateItemLabel: PropTypes.func.isRequired, }), }), /** * Used to determine if a given item can move to some new position. * @param {object} params The params describing the item re-ordering. - * @param {string} params.itemId The id of the item to check. + * @param {string} params.itemId The id of the item that is being moved to a new position. * @param {TreeViewItemReorderPosition} params.oldPosition The old position of the item. * @param {TreeViewItemReorderPosition} params.newPosition The new position of the item. * @returns {boolean} `true` if the item can move to the new position. @@ -251,6 +252,7 @@ RichTreeViewPro.propTypes = { experimentalFeatures: PropTypes.shape({ indentationAtItemLevel: PropTypes.bool, itemsReordering: PropTypes.bool, + labelEditing: PropTypes.bool, }), /** * Used to determine the id of a given item. @@ -282,6 +284,16 @@ RichTreeViewPro.propTypes = { * @returns {boolean} `true` if the item should be disabled. */ isItemDisabled: PropTypes.func, + /** + * Determines if a given item is editable or not. + * Make sure to also enable the `labelEditing` experimental feature: + * ``. + * By default, the items are not editable. + * @template R + * @param {R} item The item to check. + * @returns {boolean} `true` if the item is editable. + */ + isItemEditable: PropTypes.oneOfType([PropTypes.func, PropTypes.bool]), /** * Used to determine if a given item can be reordered. * @param {string} itemId The id of the item to check. @@ -333,6 +345,12 @@ RichTreeViewPro.propTypes = { * @param {string} itemId The id of the focused item. */ onItemFocus: PropTypes.func, + /** + * Callback fired when the label of an item changes. + * @param {TreeViewItemId} itemId The id of the item that was edited. + * @param {string} newLabel The new label of the items. + */ + onItemLabelChange: PropTypes.func, /** * Callback fired when a tree item is moved in the tree. * @param {object} params The params describing the item re-ordering. diff --git a/packages/x-tree-view-pro/src/internals/plugins/useTreeViewItemsReordering/useTreeViewItemsReordering.test.tsx b/packages/x-tree-view-pro/src/internals/plugins/useTreeViewItemsReordering/useTreeViewItemsReordering.test.tsx index b316969dbc1f..9e01b6229860 100644 --- a/packages/x-tree-view-pro/src/internals/plugins/useTreeViewItemsReordering/useTreeViewItemsReordering.test.tsx +++ b/packages/x-tree-view-pro/src/internals/plugins/useTreeViewItemsReordering/useTreeViewItemsReordering.test.tsx @@ -180,6 +180,23 @@ describeTreeView< }); describe('canMoveItemToNewPosition prop', () => { + it('should call canMoveItemToNewPosition with the correct parameters', () => { + const canMoveItemToNewPosition = spy(); + const response = render({ + experimentalFeatures: { indentationAtItemLevel: true, itemsReordering: true }, + items: [{ id: '1' }, { id: '2' }, { id: '3' }], + itemsReordering: true, + canMoveItemToNewPosition, + }); + + dragEvents.fullDragSequence(response.getItemRoot('1'), response.getItemContent('2')); + expect(canMoveItemToNewPosition.lastCall.firstArg).to.deep.equal({ + itemId: '1', + oldPosition: { parentId: null, index: 0 }, + newPosition: { parentId: null, index: 1 }, + }); + }); + it('should not allow to drop an item when canMoveItemToNewPosition returns false', () => { const response = render({ experimentalFeatures: { indentationAtItemLevel: true, itemsReordering: true }, diff --git a/packages/x-tree-view-pro/src/internals/plugins/useTreeViewItemsReordering/useTreeViewItemsReordering.ts b/packages/x-tree-view-pro/src/internals/plugins/useTreeViewItemsReordering/useTreeViewItemsReordering.ts index 91843a7b6dc4..a19c0c491e0e 100644 --- a/packages/x-tree-view-pro/src/internals/plugins/useTreeViewItemsReordering/useTreeViewItemsReordering.ts +++ b/packages/x-tree-view-pro/src/internals/plugins/useTreeViewItemsReordering/useTreeViewItemsReordering.ts @@ -55,18 +55,19 @@ export const useTreeViewItemsReordering: TreeViewPlugin { - if (!state.itemsReordering) { + const itemsReordering = state.itemsReordering; + if (!itemsReordering) { throw new Error('There is no ongoing reordering.'); } - if (itemId === state.itemsReordering.draggedItemId) { + if (itemId === itemsReordering.draggedItemId) { return {}; } const canMoveItemToNewPosition = params.canMoveItemToNewPosition; const targetItemMeta = instance.getItemMeta(itemId); const targetItemIndex = instance.getItemIndex(targetItemMeta.id); - const draggedItemMeta = instance.getItemMeta(state.itemsReordering.draggedItemId); + const draggedItemMeta = instance.getItemMeta(itemsReordering.draggedItemId); const draggedItemIndex = instance.getItemIndex(draggedItemMeta.id); const oldPosition: TreeViewItemReorderPosition = { @@ -84,7 +85,7 @@ export const useTreeViewItemsReordering: TreeViewPlugin, - UseTreeViewIconsParameters {} + UseTreeViewIconsParameters, + UseTreeViewLabelParameters {} diff --git a/packages/x-tree-view/src/RichTreeView/RichTreeView.tsx b/packages/x-tree-view/src/RichTreeView/RichTreeView.tsx index 6eb825d49fa1..5c46c54794d1 100644 --- a/packages/x-tree-view/src/RichTreeView/RichTreeView.tsx +++ b/packages/x-tree-view/src/RichTreeView/RichTreeView.tsx @@ -154,6 +154,7 @@ RichTreeView.propTypes = { getItemTree: PropTypes.func.isRequired, selectItem: PropTypes.func.isRequired, setItemExpansion: PropTypes.func.isRequired, + updateItemLabel: PropTypes.func.isRequired, }), }), /** @@ -205,6 +206,7 @@ RichTreeView.propTypes = { */ experimentalFeatures: PropTypes.shape({ indentationAtItemLevel: PropTypes.bool, + labelEditing: PropTypes.bool, }), /** * Used to determine the id of a given item. @@ -236,6 +238,16 @@ RichTreeView.propTypes = { * @returns {boolean} `true` if the item should be disabled. */ isItemDisabled: PropTypes.func, + /** + * Determines if a given item is editable or not. + * Make sure to also enable the `labelEditing` experimental feature: + * ``. + * By default, the items are not editable. + * @template R + * @param {R} item The item to check. + * @returns {boolean} `true` if the item is editable. + */ + isItemEditable: PropTypes.oneOfType([PropTypes.func, PropTypes.bool]), /** * Horizontal indentation between an item and its children. * Examples: 24, "24px", "2rem", "2em". @@ -273,6 +285,12 @@ RichTreeView.propTypes = { * @param {string} itemId The id of the focused item. */ onItemFocus: PropTypes.func, + /** + * Callback fired when the label of an item changes. + * @param {TreeViewItemId} itemId The id of the item that was edited. + * @param {string} newLabel The new label of the items. + */ + onItemLabelChange: PropTypes.func, /** * Callback fired when a tree item is selected or deselected. * @param {React.SyntheticEvent} event The DOM event that triggered the change. diff --git a/packages/x-tree-view/src/TreeItem/TreeItem.tsx b/packages/x-tree-view/src/TreeItem/TreeItem.tsx index c66a4fde6a30..2be1daaa83f5 100644 --- a/packages/x-tree-view/src/TreeItem/TreeItem.tsx +++ b/packages/x-tree-view/src/TreeItem/TreeItem.tsx @@ -26,6 +26,7 @@ import { TreeViewCollapseIcon, TreeViewExpandIcon } from '../icons'; import { TreeItem2Provider } from '../TreeItem2Provider'; import { TreeViewItemDepthContext } from '../internals/TreeViewItemDepthContext'; import { useTreeItemState } from './useTreeItemState'; +import { isTargetInDescendants } from '../internals/utils/tree'; const useThemeProps = createUseThemeProps('MuiTreeItem'); @@ -42,6 +43,9 @@ const useUtilityClasses = (ownerState: TreeItemOwnerState) => { iconContainer: ['iconContainer'], checkbox: ['checkbox'], label: ['label'], + labelInput: ['labelInput'], + editing: ['editing'], + editable: ['editable'], groupTransition: ['groupTransition'], }; @@ -217,7 +221,8 @@ export const TreeItem = React.forwardRef(function TreeItem( ...other } = props; - const { expanded, focused, selected, disabled, handleExpansion } = useTreeItemState(itemId); + const { expanded, focused, selected, disabled, editing, handleExpansion } = + useTreeItemState(itemId); const { contentRef, rootRef, propsEnhancers } = runItemPlugins(props); const rootRefObject = React.useRef(null); @@ -343,11 +348,27 @@ export const TreeItem = React.forwardRef(function TreeItem( function handleBlur(event: React.FocusEvent) { onBlur?.(event); + if ( + editing || + // we can exit the editing state by clicking outside the input (within the tree item) or by pressing Enter or Escape -> we don't want to remove the focused item from the state in these cases + // we can also exit the editing state by clicking on the root itself -> want to remove the focused item from the state in this case + (event.relatedTarget && + isTargetInDescendants(event.relatedTarget as HTMLElement, rootRefObject.current) && + ((event.target && + (event.target as HTMLElement)?.dataset?.element === 'labelInput' && + isTargetInDescendants(event.target as HTMLElement, rootRefObject.current)) || + (event.relatedTarget as HTMLElement)?.dataset?.element === 'labelInput')) + ) { + return; + } instance.removeFocusedItem(); } const handleKeyDown = (event: React.KeyboardEvent) => { onKeyDown?.(event); + if ((event.target as HTMLElement)?.dataset?.element === 'labelInput') { + return; + } instance.handleItemKeyDown(event, itemId); }; @@ -372,6 +393,12 @@ export const TreeItem = React.forwardRef(function TreeItem( contentRefObject, externalEventHandlers: {}, }) ?? {}; + const enhancedLabelInputProps = + propsEnhancers.labelInput?.({ + rootRefObject, + contentRefObject, + externalEventHandlers: {}, + }) ?? {}; return ( @@ -408,8 +435,11 @@ export const TreeItem = React.forwardRef(function TreeItem( selected: classes.selected, focused: classes.focused, disabled: classes.disabled, + editable: classes.editable, + editing: classes.editing, iconContainer: classes.iconContainer, label: classes.label, + labelInput: classes.labelInput, checkbox: classes.checkbox, }} label={label} @@ -425,6 +455,9 @@ export const TreeItem = React.forwardRef(function TreeItem( {...((enhancedDragAndDropOverlayProps as any).action == null ? {} : { dragAndDropOverlayProps: enhancedDragAndDropOverlayProps })} + {...((enhancedLabelInputProps as any).value == null + ? {} + : { labelInputProps: enhancedLabelInputProps })} ref={handleContentRef} /> {children && ( diff --git a/packages/x-tree-view/src/TreeItem/TreeItemContent.tsx b/packages/x-tree-view/src/TreeItem/TreeItemContent.tsx index 686d7d9c88a5..2c62bd29c452 100644 --- a/packages/x-tree-view/src/TreeItem/TreeItemContent.tsx +++ b/packages/x-tree-view/src/TreeItem/TreeItemContent.tsx @@ -7,6 +7,8 @@ import { TreeItem2DragAndDropOverlay, TreeItem2DragAndDropOverlayProps, } from '../TreeItem2DragAndDropOverlay'; +import { TreeItem2LabelInput, TreeItem2LabelInputProps } from '../TreeItem2LabelInput'; +import { MuiCancellableEvent } from '../internals/models'; export interface TreeItemContentProps extends React.HTMLAttributes { className?: string; @@ -30,6 +32,12 @@ export interface TreeItemContentProps extends React.HTMLAttributes label: string; /** Styles applied to the checkbox element. */ checkbox: string; + /** Styles applied to the input element that is visible when editing is enabled. */ + labelInput: string; + /** Styles applied to the content element when editing is enabled. */ + editing: string; + /** Styles applied to the content of the items that are editable. */ + editable: string; }; /** * The tree item label. @@ -52,6 +60,7 @@ export interface TreeItemContentProps extends React.HTMLAttributes */ displayIcon?: React.ReactNode; dragAndDropOverlayProps?: TreeItem2DragAndDropOverlayProps; + labelInputProps?: TreeItem2LabelInputProps; } export type TreeItemContentClassKey = keyof NonNullable; @@ -74,6 +83,7 @@ const TreeItemContent = React.forwardRef(function TreeItemContent( onClick, onMouseDown, dragAndDropOverlayProps, + labelInputProps, ...other } = props; @@ -82,6 +92,8 @@ const TreeItemContent = React.forwardRef(function TreeItemContent( expanded, selected, focused, + editing, + editable, disableSelection, checkboxSelection, handleExpansion, @@ -90,6 +102,9 @@ const TreeItemContent = React.forwardRef(function TreeItemContent( handleContentClick, preventSelection, expansionTrigger, + toggleItemEditing, + handleSaveItemLabel, + handleCancelItemLabelEditing, } = useTreeItemState(itemId); const icon = iconProp || expansionIcon || displayIcon; @@ -123,6 +138,39 @@ const TreeItemContent = React.forwardRef(function TreeItemContent( } }; + const handleLabelDoubleClick = (event: React.MouseEvent & MuiCancellableEvent) => { + if (event.defaultMuiPrevented) { + return; + } + 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 */
)} -
{label}
+ {editing ? ( + + ) : ( +
+ {label} +
+ )} + {dragAndDropOverlayProps && }
); @@ -189,6 +251,7 @@ TreeItemContent.propTypes = { * The tree item label. */ label: PropTypes.node, + labelInputProps: PropTypes.object, } as any; export { TreeItemContent }; diff --git a/packages/x-tree-view/src/TreeItem/treeItemClasses.ts b/packages/x-tree-view/src/TreeItem/treeItemClasses.ts index a8ac533418c6..6b0efd7279ce 100644 --- a/packages/x-tree-view/src/TreeItem/treeItemClasses.ts +++ b/packages/x-tree-view/src/TreeItem/treeItemClasses.ts @@ -22,6 +22,12 @@ export interface TreeItemClasses { label: string; /** Styles applied to the checkbox element. */ checkbox: string; + /** Styles applied to the input element that is visible when editing is enabled. */ + labelInput: string; + /** Styles applied to the content element when editing is enabled. */ + editing: string; + /** Styles applied to the content of the items that are editable. */ + editable: string; /** Styles applied to the drag and drop overlay. */ dragAndDropOverlay: string; } @@ -43,5 +49,8 @@ export const treeItemClasses: TreeItemClasses = generateUtilityClasses('MuiTreeI 'iconContainer', 'label', 'checkbox', + 'labelInput', + 'editable', + 'editing', 'dragAndDropOverlay', ]); diff --git a/packages/x-tree-view/src/TreeItem/useTreeItemState.ts b/packages/x-tree-view/src/TreeItem/useTreeItemState.ts index 7f41a5ad7dcc..a236983664ee 100644 --- a/packages/x-tree-view/src/TreeItem/useTreeItemState.ts +++ b/packages/x-tree-view/src/TreeItem/useTreeItemState.ts @@ -1,9 +1,12 @@ import * as React from 'react'; +import { MuiCancellableEvent } from '../internals/models/MuiCancellableEvent'; import { useTreeViewContext } from '../internals/TreeViewProvider'; import { UseTreeViewSelectionSignature } from '../internals/plugins/useTreeViewSelection'; import { UseTreeViewExpansionSignature } from '../internals/plugins/useTreeViewExpansion'; import { UseTreeViewFocusSignature } from '../internals/plugins/useTreeViewFocus'; import { UseTreeViewItemsSignature } from '../internals/plugins/useTreeViewItems'; +import { UseTreeViewLabelSignature, useTreeViewLabel } from '../internals/plugins/useTreeViewLabel'; +import { hasPlugin } from '../internals/utils/plugins'; type UseTreeItemStateMinimalPlugins = readonly [ UseTreeViewSelectionSignature, @@ -12,7 +15,7 @@ type UseTreeItemStateMinimalPlugins = readonly [ UseTreeViewItemsSignature, ]; -type UseTreeItemStateOptionalPlugins = readonly []; +type UseTreeItemStateOptionalPlugins = readonly [UseTreeViewLabelSignature]; export function useTreeItemState(itemId: string) { const { @@ -27,6 +30,8 @@ export function useTreeItemState(itemId: string) { const focused = instance.isItemFocused(itemId); const selected = instance.isItemSelected(itemId); const disabled = instance.isItemDisabled(itemId); + const editing = instance?.isItemBeingEdited ? instance?.isItemBeingEdited(itemId) : false; + const editable = instance.isItemEditable ? instance.isItemEditable(itemId) : false; const handleExpansion = (event: React.MouseEvent) => { if (!disabled) { @@ -87,11 +92,56 @@ export function useTreeItemState(itemId: string) { } }; + const toggleItemEditing = () => { + if (!hasPlugin(instance, useTreeViewLabel)) { + return; + } + if (instance.isItemEditable(itemId)) { + if (instance.isItemBeingEdited(itemId)) { + instance.setEditedItemId(null); + } else { + instance.setEditedItemId(itemId); + } + } + }; + + const handleSaveItemLabel = ( + event: React.SyntheticEvent & MuiCancellableEvent, + label: string, + ) => { + if (!hasPlugin(instance, useTreeViewLabel)) { + return; + } + + // As a side effect of `instance.focusItem` called here and in `handleCancelItemLabelEditing` the `labelInput` is blurred + // The `onBlur` event is triggered, which calls `handleSaveItemLabel` again. + // To avoid creating an unwanted behavior we need to check if the item is being edited before calling `updateItemLabel` + // using `instance.isItemBeingEditedRef` instead of `instance.isItemBeingEdited` since the state is not yet updated in this point + if (instance.isItemBeingEditedRef(itemId)) { + instance.updateItemLabel(itemId, label); + toggleItemEditing(); + instance.focusItem(event, itemId); + } + }; + + const handleCancelItemLabelEditing = (event: React.SyntheticEvent) => { + if (!hasPlugin(instance, useTreeViewLabel)) { + return; + } + + if (instance.isItemBeingEditedRef(itemId)) { + toggleItemEditing(); + instance.focusItem(event, itemId); + } + }; + return { disabled, expanded, selected, focused, + editable, + editing, disableSelection, checkboxSelection, handleExpansion, @@ -100,5 +150,8 @@ export function useTreeItemState(itemId: string) { handleContentClick: onItemClick, preventSelection, expansionTrigger, + toggleItemEditing, + handleSaveItemLabel, + handleCancelItemLabelEditing, }; } diff --git a/packages/x-tree-view/src/TreeItem2/TreeItem2.tsx b/packages/x-tree-view/src/TreeItem2/TreeItem2.tsx index a1f72bfc089d..c792574ce229 100644 --- a/packages/x-tree-view/src/TreeItem2/TreeItem2.tsx +++ b/packages/x-tree-view/src/TreeItem2/TreeItem2.tsx @@ -13,12 +13,14 @@ import { TreeItem2Props, TreeItem2OwnerState } from './TreeItem2.types'; import { unstable_useTreeItem2 as useTreeItem2, UseTreeItem2ContentSlotOwnProps, + UseTreeItem2LabelSlotOwnProps, UseTreeItem2Status, } from '../useTreeItem2'; import { getTreeItemUtilityClass } from '../TreeItem'; import { TreeItem2Icon } from '../TreeItem2Icon'; import { TreeItem2DragAndDropOverlay } from '../TreeItem2DragAndDropOverlay'; import { TreeItem2Provider } from '../TreeItem2Provider'; +import { TreeItem2LabelInput } from '../TreeItem2LabelInput'; const useThemeProps = createUseThemeProps('MuiTreeItem2'); @@ -115,13 +117,23 @@ export const TreeItem2Label = styled('div', { name: 'MuiTreeItem2', slot: 'Label', overridesResolver: (props, styles) => styles.label, -})(({ theme }) => ({ + shouldForwardProp: (prop) => shouldForwardProp(prop) && prop !== 'editable', +})<{ editable?: boolean }>(({ theme }) => ({ width: '100%', boxSizing: 'border-box', // prevent width + padding to overflow // fixes overflow - see https://github.com/mui/material-ui/issues/27372 minWidth: 0, position: 'relative', + overflow: 'hidden', ...theme.typography.body1, + variants: [ + { + props: ({ editable }: UseTreeItem2LabelSlotOwnProps) => editable, + style: { + paddingLeft: '2px', + }, + }, + ], })); export const TreeItem2IconContainer = styled('div', { @@ -182,6 +194,8 @@ const useUtilityClasses = (ownerState: TreeItem2OwnerState) => { root: ['root'], content: ['content'], expanded: ['expanded'], + editing: ['editing'], + editable: ['editable'], selected: ['selected'], focused: ['focused'], disabled: ['disabled'], @@ -189,6 +203,7 @@ const useUtilityClasses = (ownerState: TreeItem2OwnerState) => { checkbox: ['checkbox'], label: ['label'], groupTransition: ['groupTransition'], + labelInput: ['labelInput'], dragAndDropOverlay: ['dragAndDropOverlay'], }; @@ -224,6 +239,7 @@ export const TreeItem2 = React.forwardRef(function TreeItem2( getCheckboxProps, getLabelProps, getGroupTransitionProps, + getLabelInputProps, getDragAndDropOverlayProps, status, } = useTreeItem2({ @@ -265,8 +281,11 @@ export const TreeItem2 = React.forwardRef(function TreeItem2( [classes.selected]: status.selected, [classes.focused]: status.focused, [classes.disabled]: status.disabled, + [classes.editing]: status.editing, + [classes.editable]: status.editable, }), }); + const IconContainer: React.ElementType = slots.iconContainer ?? TreeItem2IconContainer; const iconContainerProps = useSlotProps({ elementType: IconContainer, @@ -303,6 +322,15 @@ export const TreeItem2 = React.forwardRef(function TreeItem2( className: classes.groupTransition, }); + const LabelInput: React.ElementType = slots.labelInput ?? TreeItem2LabelInput; + const labelInputProps = useSlotProps({ + elementType: LabelInput, + getSlotProps: getLabelInputProps, + externalSlotProps: slotProps.labelInput, + ownerState: {}, + className: classes.labelInput, + }); + const DragAndDropOverlay: React.ElementType | undefined = slots.dragAndDropOverlay ?? TreeItem2DragAndDropOverlay; const dragAndDropOverlayProps = useSlotProps({ @@ -321,7 +349,7 @@ export const TreeItem2 = React.forwardRef(function TreeItem2( -