Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
57 commits
Select commit Hold shift + click to select a range
9fbbf84
Integrate useEditable() and pass controller to abstractCreateDemoClie…
dav-is Mar 20, 2026
2ba514b
Integrate enhancers in CodeProvider and CodeControllerContext
dav-is Mar 20, 2026
7c87a7b
Improve comment handling when editing source
dav-is Mar 20, 2026
f1841bc
Add highlighted section to editing demo
dav-is Mar 20, 2026
7b7ebb1
Prettier
dav-is Mar 20, 2026
8998ead
Update docs
dav-is Mar 20, 2026
ae72b05
prettier
dav-is Mar 20, 2026
e5f9417
Merge branch 'master' into docs-infra/improve-live-editing
dav-is Mar 20, 2026
3244d33
Merge branch 'master' into docs-infra/improve-live-editing
dav-is Mar 23, 2026
67e949c
Merge branch 'master' into docs-infra/improve-live-editing
dav-is Mar 23, 2026
70b2a3e
Fork useEditable()
dav-is Mar 24, 2026
29a1267
Fix lint
dav-is Mar 24, 2026
0b71a73
Add useEditable tests
dav-is Mar 24, 2026
9599f10
feat: enhance useEditable for better text insertion and handling in c…
dav-is Apr 7, 2026
5d9e6f0
Fix workspace
dav-is Apr 7, 2026
4580dbb
Merge branch 'master' into docs-infra/improve-live-editing
dav-is Apr 10, 2026
7a208b4
Prettier
dav-is Apr 10, 2026
8d7038f
Merge branch 'master' into docs-infra/improve-live-editing
dav-is Apr 16, 2026
72ef791
Optimize useEditable
dav-is Apr 22, 2026
8c769cf
Fix issue when holding down key on Firefox
dav-is Apr 22, 2026
5ced874
Add benchmark
dav-is Apr 22, 2026
cb69ffe
Fix cursor shifting
dav-is Apr 22, 2026
c7c62df
Fix initial undo stack state
dav-is Apr 22, 2026
f5c35ca
Fix undo stack when pressing enter
dav-is Apr 22, 2026
7ca44b0
Improve for React 19
dav-is Apr 22, 2026
703ddd6
Improve typescript types
dav-is Apr 22, 2026
c7363e9
Upgrade playwright
dav-is Apr 22, 2026
9511277
Optimize useSourceEditing
dav-is Apr 22, 2026
dd08e27
Update types.md
dav-is Apr 22, 2026
31dcfc7
Fix firefox bug
dav-is Apr 22, 2026
917dad8
Fix highlight state bug
dav-is Apr 22, 2026
c3f2f9f
Handle removing empty lines at the start of a region
dav-is Apr 22, 2026
0a63583
Prevent firefox enter keydown bug
dav-is Apr 22, 2026
4f0ab0c
Improve firefox undo stack
dav-is Apr 22, 2026
e88e092
Merge branch 'master' into docs-infra/improve-live-editing
dav-is Apr 22, 2026
e1aa3c3
bump version
dav-is Apr 22, 2026
b11d870
Merge branch 'master' into docs-infra/improve-live-editing
dav-is Apr 24, 2026
d67f138
Add missing `@focus`
dav-is Apr 24, 2026
bc9b0b6
Fix lint
dav-is Apr 25, 2026
992f801
Merge branch 'master' into docs-infra/improve-live-editing
dav-is Apr 25, 2026
1a6585e
Fix resetFocus()
dav-is Apr 27, 2026
c6a0aa5
validate docs
dav-is Apr 27, 2026
021d8a6
Merge branch 'master' into docs-infra/improve-live-editing
dav-is Apr 27, 2026
449ad99
Opt into indenting
dav-is Apr 27, 2026
fd20fc3
Improve collapsed editable behavior
dav-is Apr 27, 2026
63d514a
Merge branch 'docs-infra/improve-live-editing' of github.com:mui/mui-…
dav-is Apr 27, 2026
8f3570d
Improve caret behavior
dav-is Apr 28, 2026
ff387f9
Prettier
dav-is Apr 28, 2026
a0f3663
Fix lint
dav-is Apr 28, 2026
d36d97a
Fix cursor edge case
dav-is Apr 28, 2026
33be548
Fix backspace in collapsed
dav-is Apr 28, 2026
9c7289b
Performance improvements
dav-is Apr 28, 2026
1446d21
Improve TS support
dav-is Apr 28, 2026
d08a735
Refactor
dav-is Apr 28, 2026
6b414be
Merge branch 'master' into docs-infra/improve-live-editing
dav-is Apr 28, 2026
43c0650
Update changes list
dav-is Apr 28, 2026
e50ae06
Merge branch 'master' into docs-infra/improve-live-editing
dav-is Apr 28, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ jobs:
test_benchmark:
executor:
name: code-infra/mui-node-browser
playwright-img-version: 'v1.58.2-noble'
playwright-img-version: 'v1.59.1-noble'
resource_class: medium
steps:
- checkout
Expand Down
2 changes: 2 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ Always reference these instructions first and fallback to search or bash command
- **Formatting**: `pnpm prettier` -- always run before pushing code.
- **Run tests**: `pnpm test --run` takes 5-10 seconds. **NEVER CANCEL**. Set timeout to 30+ minutes.
- **Run specific tests**: `pnpm test --run loadServerSource` or `pnpm test --run integration.test.ts` for targeted testing
- **Run browser tests**: `pnpm test:browser --run` -- requires podman or docker. Starts a containerized Playwright server and runs browser tests against it.
- **Run browser tests in CI**: In a `mcr.microsoft.com/playwright` container image, run `pnpm test:browser:unconfined` directly — no container engine needed. Only use in a CI environment.
- **ALWAYS use `--run` flag** to avoid watch mode when running tests programmatically
- **Do NOT use `--`** in test commands (e.g., avoid `pnpm test -- --run`)
- **Use VS Code Vitest extension** whenever possible for interactive test development and debugging
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import { createDemoPerformance } from '@/functions/createDemoPerformance';
import Page from './page';

export const DemoCodeControllerContextPerformance = createDemoPerformance(import.meta.url, Page);
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import * as React from 'react';
import { createParseSource } from '@mui/internal-docs-infra/pipeline/parseSource';
import { CodeHighlighter } from '@mui/internal-docs-infra/CodeHighlighter';
import { CodeProvider } from '@mui/internal-docs-infra/CodeProvider';
import { CodeController } from '../../../../../../docs-infra/components/code-controller-context/demos/code-editor/CodeController';
import { CodeEditorContent } from '../../../../../../docs-infra/components/code-controller-context/demos/code-editor/CodeEditorContent';

import code from '../../../code-highlighter/snippets/large/snippet';

const sourceParser = createParseSource();

export default function Page() {
return (
// @focus-start
<CodeProvider>
<CodeController>
<CodeHighlighter
Content={CodeEditorContent}
controlled
sourceParser={sourceParser}
fileName="large-file.js"
>
{code}
</CodeHighlighter>
</CodeController>
</CodeProvider>
// @focus-end
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# Benchmarking Code Controller Context

This demo showcases the performance of the [`CodeControllerContext`](../../../../docs-infra/components/code-controller-context/page.mdx) component when handling large code snippets.
It uses RSC to highlight the initial code, and then highlights updates on the client side as the code is edited.

import { DemoCodeControllerContextPerformance } from './demos/code';

<DemoCodeControllerContextPerformance />

[See Setup](./demos/code/)
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import BenchCodeControllerContextPage from '@/app/bench/docs-infra/components/code-controller-context/page.mdx';

export default BenchCodeControllerContextPage;

[See Benchmarks](/docs/app/bench/docs-infra/components/code-controller-context/page.mdx)
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
'use client';

import * as React from 'react';
import { useEditable } from 'use-editable';
import type { ContentProps } from '@mui/internal-docs-infra/CodeHighlighter/types';
import { useCode } from '@mui/internal-docs-infra/useCode';
import { LabeledSwitch } from '@/components/LabeledSwitch';
Expand All @@ -10,9 +9,7 @@ import styles from './CodeEditorContent.module.css';
import '../../../code-highlighter/demos/syntax.css';

export function CodeEditorContent(props: ContentProps<object>) {
// @focus-start @padding 1
const preRef = React.useRef<HTMLPreElement | null>(null);
const code = useCode(props, { preClassName: styles.codeBlock, preRef });
const code = useCode(props, { preClassName: styles.codeBlock });

const hasJsTransform = code.availableTransforms.includes('js');
const isJsSelected = code.selectedTransform === 'js';
Expand All @@ -24,15 +21,6 @@ export function CodeEditorContent(props: ContentProps<object>) {
[code],
);

const onInput = React.useCallback(
(text: string) => {
code.setSource?.(text);
},
[code],
);

useEditable(preRef, onInput, { indentation: 2 });

return (
<div className={styles.container}>
<div className={styles.header}>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,15 +1,12 @@
import * as React from 'react';
import { CodeProvider } from '@mui/internal-docs-infra/CodeProvider';
import { DemoController } from './DemoController';
import { DemoCheckboxBasic } from './demo-basic';

export function DemoLive() {
return (
// @focus-start
<CodeProvider>
<DemoController>
<DemoCheckboxBasic />
</DemoController>
<DemoCheckboxBasic />
</CodeProvider>
// @focus-end
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -85,3 +85,8 @@
.codeBlock:focus-visible {
outline: none;
}

.codeBlock :global(.frame)[data-frame-type='highlighted'] {
background: #ebe4ff;
border-radius: 8px;
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
'use client';

import * as React from 'react';
import { useEditable } from 'use-editable';
import type { ContentProps } from '@mui/internal-docs-infra/CodeHighlighter/types';
import { useDemo } from '@mui/internal-docs-infra/useDemo';
import { LabeledSwitch } from '@/components/LabeledSwitch';
Expand All @@ -16,9 +15,7 @@ const variantNames: Record<string, string | undefined> = {
};

export function DemoLiveContent(props: ContentProps<object>) {
// @focus-start @padding 1
const preRef = React.useRef<HTMLPreElement | null>(null);
const demo = useDemo(props, { preClassName: styles.codeBlock, preRef });
const demo = useDemo(props, { preClassName: styles.codeBlock });

const hasJsTransform = demo.availableTransforms.includes('js');
const isJsSelected = demo.selectedTransform === 'js';
Expand All @@ -41,14 +38,6 @@ export function DemoLiveContent(props: ContentProps<object>) {
[demo.variants],
);

const onChange = React.useCallback(
(text: string) => {
demo.setSource?.(text);
},
[demo],
);
useEditable(preRef, onChange, { indentation: 2, disabled: !demo.setSource });

return (
<div className={styles.container}>
<div className={styles.demoSection}>{demo.component}</div>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
'use client';

import { createDemoClientFactory } from '@mui/internal-docs-infra/abstractCreateDemoClient';
import { DemoController } from './DemoController';

/**
* Creates a demo client copying dependencies in the client bundle for live editing.
* @param url Depends on `import.meta.url` to determine the source file location.
* @param meta Additional meta and modules for the demo client.
*/
export const createDemoClient = createDemoClientFactory({
live: true,
DemoController,
});
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
'use client';

import * as React from 'react';
import { useEditable } from 'use-editable';
import type { ContentProps } from '@mui/internal-docs-infra/CodeHighlighter/types';
import { useCode } from '@mui/internal-docs-infra/useCode';
import { Tabs } from '@/components/Tabs';
Expand All @@ -10,9 +9,7 @@ import styles from '../code-editor/CodeEditorContent.module.css';
import '../../../code-highlighter/demos/syntax.css';

export function MultiFileContent(props: ContentProps<object>) {
// @focus-start @padding 1
const preRef = React.useRef<HTMLPreElement | null>(null);
const code = useCode(props, { preClassName: styles.codeBlock, preRef });
const code = useCode(props, { preClassName: styles.codeBlock });

const tabs = React.useMemo(() => {
return code.files.map(({ name }) => ({
Expand All @@ -21,15 +18,6 @@ export function MultiFileContent(props: ContentProps<object>) {
}));
}, [code.files]);

const onInput = React.useCallback(
(text: string) => {
code.setSource?.(text);
},
[code],
);

useEditable(preRef, onInput, { indentation: 2 });

return (
<div className={styles.container}>
<div className={styles.header}>
Expand Down
47 changes: 15 additions & 32 deletions docs/app/docs-infra/components/code-controller-context/page.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -137,22 +137,10 @@ The [`useCode`](../../hooks/use-code/page.mdx) hook automatically integrates wit
```tsx
'use client';

import { useEditable } from 'use-editable';
import { useCode } from '@mui/internal-docs-infra/useCode';

function EditorContent(props) {
const preRef = React.useRef<HTMLPreElement | null>(null);
const code = useCode(props, { preRef });

// code.setSource updates the controller automatically
const onInput = React.useCallback(
(text: string) => {
code.setSource?.(text);
},
[code],
);

useEditable(preRef, onInput, { indentation: 2 });
const code = useCode(props);

return (
<div>
Expand All @@ -165,7 +153,8 @@ function EditorContent(props) {

## Working with useDemo Hook

The [`useDemo`](../../hooks/use-demo/page.mdx) hook provides additional functionality for live component demos:
The [`useDemo`](../../hooks/use-demo/page.mdx) hook provides additional functionality for live component demos.
Editing is handled automatically by the `Pre` component — just render `demo.selectedFile`:

```tsx
'use client';
Expand All @@ -175,19 +164,12 @@ import { useDemo } from '@mui/internal-docs-infra/useDemo';
function DemoContent(props) {
const demo = useDemo(props);

const onInput = React.useCallback(
(text: string) => {
demo.setSource?.(text);
},
[demo],
);

return (
<div>
{/* Live component preview */}
<div>{demo.component}</div>

{/* Editable source code */}
{/* Editable source code — editing is handled automatically */}
<div>
<select value={demo.selectedVariant} onChange={(e) => demo.selectVariant(e.target.value)}>
{demo.variants.map((variant) => (
Expand All @@ -197,7 +179,7 @@ function DemoContent(props) {
))}
</select>

<textarea value={demo.selectedFile} onChange={(e) => onInput(e.target.value)} />
{demo.selectedFile}
</div>
</div>
);
Expand All @@ -210,22 +192,18 @@ The CodeController provides a simple state management pattern:

1. **Initial State**: Controller starts with empty state (`code: undefined`)
2. **Code Loading**: [`CodeHighlighter`](../code-highlighter/page.mdx) loads initial code from file or props
3. **User Interaction**: When users edit code, `setSource` calls `controlledSetCode`
3. **User Interaction**: When users edit code in the `Pre` component, `setSource` updates the controller
4. **State Update**: Controller state updates and all connected components re-render
5. **Live Preview**: For demo controllers, components are regenerated from updated code

```tsx
// Flow: User Edit → setSource → controlledSetCode → Context Update → Re-render
// Flow: User Edit (in Pre) → setSource → controlledSetCode → Context Update → Re-render

function EditorContent(props) {
const code = useCode(props); // Connects to controller context

const onEdit = (newSource) => {
code.setSource(newSource); // This updates the controller context
};

// code.selectedFile reflects the current controller state
return <textarea value={code.selectedFile} onChange={onEdit} />;
// code.selectedFile is a <Pre> component that handles editing automatically
return <div>{code.selectedFile}</div>;
}
```

Expand Down Expand Up @@ -282,14 +260,15 @@ You can also create your own `createDemoClient` using the abstract factory:
'use client';

import { createDemoClientFactory } from '@mui/internal-docs-infra/abstractCreateDemoClient';
import { DemoController } from './DemoController';

/**
* Creates a demo client copying dependencies in the client bundle for live editing.
* @param url Depends on `import.meta.url` to determine the source file location.
* @param meta Additional meta and modules for the demo client.
*/
export const createDemoClient = createDemoClientFactory({
live: true,
DemoController,
});
```

Expand Down Expand Up @@ -318,6 +297,10 @@ The `ClientProvider` ensures that:

This is only needed for demos that render live components from editable code, not for simple code editor scenarios.

## Benchmarking

For performance benchmarks of the `CodeControllerContext` component, see the [Benchmarking Code Controller Context](./bench/page.mdx) page.

## Best Practices

1. **Use with [`CodeProvider`](../code-provider/page.mdx)** - Always wrap CodeController with [`CodeProvider`](../code-provider/page.mdx) for client-side highlighting
Expand Down
32 changes: 25 additions & 7 deletions docs/app/docs-infra/components/code-controller-context/types.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,13 +23,14 @@ An object containing:
- setSelection: Function to update the selection
- components: Override components for the preview

| Property | Type | Description |
| :----------- | :------------------------------------------------------------------------------- | :---------- |
| code | `ControlledCode \| undefined` | - |
| selection | `Selection \| undefined` | - |
| setCode | `React.Dispatch<React.SetStateAction<ControlledCode \| undefined>> \| undefined` | - |
| setSelection | `React.Dispatch<React.SetStateAction<Selection>> \| undefined` | - |
| components | `Record<string, React.ReactNode> \| undefined` | - |
| Property | Type | Description |
| :-------------- | :------------------------------------------------------------------------------- | :---------- |
| code | `ControlledCode \| undefined` | - |
| selection | `Selection \| undefined` | - |
| setCode | `React.Dispatch<React.SetStateAction<ControlledCode \| undefined>> \| undefined` | - |
| setSelection | `React.Dispatch<React.SetStateAction<Selection>> \| undefined` | - |
| components | `Record<string, React.ReactNode> \| undefined` | - |
| sourceEnhancers | `SourceEnhancer[] \| undefined` | - |

## Additional Types

Expand Down Expand Up @@ -73,6 +74,11 @@ type CodeControllerContext = {
* e.g. `{ variantA: {}, variantB: {} }`.
*/
components?: Record<string, React.ReactNode>;
/**
* Additional source enhancers to apply to parsed HAST sources.
* These are merged with enhancers from CodeProvider and useCode opts.
*/
sourceEnhancers?: SourceEnhancer[];
};
```

Expand All @@ -81,3 +87,15 @@ type CodeControllerContext = {
```typescript
type Selection = { variant: string; fileName?: string; transformKey?: string };
```

## External Types

### SourceEnhancer

```typescript
type SourceEnhancer = (
root: { data?: unknown | undefined },
comments: {} | undefined,
fileName: string,
) => { data?: unknown | undefined } | Promise;
```
Loading
Loading