diff --git a/packages/toolpad-app/src/runtime/ToolpadApp.spec.tsx b/packages/toolpad-app/src/runtime/ToolpadApp.spec.tsx index 9ee7e66eb4d..5c14c05d333 100644 --- a/packages/toolpad-app/src/runtime/ToolpadApp.spec.tsx +++ b/packages/toolpad-app/src/runtime/ToolpadApp.spec.tsx @@ -70,7 +70,7 @@ test(`Default Text`, async () => { await waitFor(() => screen.getByTestId('page-root')); - const text = screen.getByText('', { selector: 'p' }); + const text = screen.getByText('text', { selector: 'p' }); expect(text).toHaveClass('MuiTypography-root'); }); diff --git a/packages/toolpad-components/src/Text.tsx b/packages/toolpad-components/src/Text.tsx index 893b7a142f6..af4c6a6e188 100644 --- a/packages/toolpad-components/src/Text.tsx +++ b/packages/toolpad-components/src/Text.tsx @@ -6,12 +6,29 @@ import { Link as MuiLink, LinkProps as MuiLinkProps, styled, + TextareaAutosize, } from '@mui/material'; -import { createComponent } from '@mui/toolpad-core'; +import { createComponent, useNode } from '@mui/toolpad-core'; +import { Typography } from '@mui/material/styles/createTypography'; import { SX_PROP_HELPER_TEXT } from './constants'; const Markdown = React.lazy(() => import('markdown-to-jsx')); +const StyledTextareaAutosize = styled(TextareaAutosize)(({ theme }) => ({ + width: '100%', + resize: 'none', + border: 'none', + outline: 'none', + padding: 0, + + ...Object.fromEntries( + Object.keys(theme.typography).map((variant) => [ + [`&.variant-${variant}`], + theme.typography[variant as keyof Typography], + ]), + ), +})); + type BaseProps = MuiLinkProps | MuiTypographyProps; interface TextProps extends Omit { mode: 'markdown' | 'link' | 'text'; @@ -57,7 +74,22 @@ const CodeContainer = styled('pre')(({ theme }) => ({ overflow: 'auto', })); +function parseInput(text: unknown): string { + return String(text).replaceAll('\n', ''); +} + function Text({ value, markdown, href, loading, mode, sx, ...rest }: TextProps) { + const [contentEditable, setContentEditable] = React.useState(null); + const [input, setInput] = React.useState(parseInput(value)); + React.useEffect(() => { + setInput(parseInput(value)); + }, [value]); + + const nodeRuntime = useNode(); + switch (mode) { case 'markdown': return loading ? ( @@ -107,18 +139,56 @@ function Text({ value, markdown, href, loading, mode, sx, ...rest }: TextProps) ); case 'text': default: - return ( + return contentEditable ? ( + { + setInput(parseInput(event.target.value)); + }} + onKeyDown={(event) => { + if (event.key === 'Enter') { + event.preventDefault(); + } + }} + autoFocus + onFocus={(event) => { + event.currentTarget.selectionStart = contentEditable.selectionStart; + event.currentTarget.selectionEnd = Math.max( + contentEditable.selectionStart, + contentEditable.selectionEnd, + ); + }} + onBlur={() => { + setContentEditable(null); + if (nodeRuntime) { + nodeRuntime.updateAppDomConstProp('value', input); + } + }} + className={`variant-${rest.variant}`} + /> + ) : ( { + if (nodeRuntime) { + const selection = window.getSelection(); + setContentEditable({ + selectionStart: selection?.anchorOffset || 0, + selectionEnd: selection?.focusOffset || 0, + }); + } }} {...rest} > - {loading ? : String(value)} + {loading ? : input} ); } @@ -139,7 +209,7 @@ export default createComponent(Text, { }, value: { helperText: 'The text content.', - typeDef: { type: 'string', default: '' }, + typeDef: { type: 'string', default: 'text' }, label: 'Value', control: { type: 'markdown' }, },