diff --git a/.changeset/inspector-mixed-children-passthrough.md b/.changeset/inspector-mixed-children-passthrough.md
new file mode 100644
index 00000000..aaf00669
--- /dev/null
+++ b/.changeset/inspector-mixed-children-passthrough.md
@@ -0,0 +1,5 @@
+---
+"@open-slide/core": patch
+---
+
+Resolve inspector text edits for host elements that mix a `{children}` or prop pass-through with other children.
diff --git a/packages/core/src/editing/edit-ops.test.ts b/packages/core/src/editing/edit-ops.test.ts
index c607e20c..9a7aafcd 100644
--- a/packages/core/src/editing/edit-ops.test.ts
+++ b/packages/core/src/editing/edit-ops.test.ts
@@ -735,6 +735,43 @@ describe('applyEdit / set-text', () => {
expect(r.source).toContain('
` mixes a nested `{label}` span with the `{children}` slot, so the
+ // `{children}` pass-through is not the element's only child. With no prevText the edit
+ // targets the host element directly, which must still resolve to the call site instead
+ // of failing with "no editable text".
+ const src = [
+ 'const Row = ({ label, children }) => (',
+ '
{label}{children}
',
+ ');',
+ 'export default [() => (',
+ '
Hello
',
+ ')];',
+ '',
+ ].join('\n');
+ const r = applyEdit(src, 2, 2, [{ kind: 'set-text', value: 'Goodbye' }]);
+ if (!r.ok) throw new Error(`expected ok, got ${r.error}`);
+ expect(r.source).toContain('
Goodbye
');
+ expect(r.source).toContain('
{label}{children}
');
+ });
+
+ it('routes a mixed-children prop pass-through to the matching call site', () => {
+ // The `
` mixes a `{children}` slot with a `{title}` prop pass-through; editing the
+ // `{title}` text resolves to the `title="..."` literal at the call site.
+ const src = [
+ 'const Banner = ({ title, children }: { title: string; children: unknown }) => (',
+ '
{children}{title}
',
+ ');',
+ 'export default [() => (',
+ '
body',
+ ')];',
+ '',
+ ].join('\n');
+ const r = applyEdit(src, 2, 2, [{ kind: 'set-text', value: 'Goodbye', prevText: 'Hello' }]);
+ if (!r.ok) throw new Error(`expected ok, got ${r.error}`);
+ expect(r.source).toContain('
body');
+ });
+
it('disambiguates between sibling call sites of a children-slot component', () => {
const src = [
'const Eyebrow = ({ children }) => (',
diff --git a/packages/core/src/editing/edit-ops.ts b/packages/core/src/editing/edit-ops.ts
index 2686a583..088a8585 100644
--- a/packages/core/src/editing/edit-ops.ts
+++ b/packages/core/src/editing/edit-ops.ts
@@ -787,12 +787,18 @@ function buildTextRangeStyleSplices(
// `
{children}` and `
{title}
` — sole child is a
// JSXExpressionContainer wrapping a bare Identifier. Returns the identifier
// name; callers branch on `'children'` vs. a generic prop passthrough.
-function propPassthroughName(element: t.JSXElement): string | null {
- const meaningful = meaningfulChildren(element);
- if (meaningful.length !== 1) return null;
- const child = meaningful[0];
- if (!t.isJSXExpressionContainer(child)) return null;
- return t.isIdentifier(child.expression) ? child.expression.name : null;
+// Names of `{prop}` / `{children}` pass-throughs among the element's *direct* children.
+// A host element can mix a pass-through with other content
+// (e.g. `
{label}{children}
`), so we don't require the pass-through to be
+// the sole child — only that it is a direct child expression container wrapping a bare identifier.
+function directPassthroughNames(element: t.JSXElement): string[] {
+ const names: string[] = [];
+ for (const child of element.children) {
+ if (t.isJSXExpressionContainer(child) && t.isIdentifier(child.expression)) {
+ names.push(child.expression.name);
+ }
+ }
+ return names;
}
type EnclosingComponent = {
@@ -1028,12 +1034,19 @@ function collectElementTextCandidates(ast: t.File, element: t.JSXElement): TextC
const candidates: TextCandidate[] = [];
collectTextCandidates(element, candidates);
if (candidates.length === 0) {
- const passthrough = propPassthroughName(element);
- const enclosing = passthrough ? findEnclosingComponent(ast, element) : null;
- if (passthrough === 'children' && enclosing) {
- candidates.push(...collectCallSiteCandidates(ast, enclosing.name));
- } else if (passthrough && enclosing && componentDestructuresProp(enclosing.fn, passthrough)) {
- candidates.push(...collectPropCallSiteCandidates(ast, enclosing.name, passthrough));
+ const passthroughs = directPassthroughNames(element);
+ const enclosing = passthroughs.length > 0 ? findEnclosingComponent(ast, element) : null;
+ if (enclosing) {
+ const seen = new Set
();
+ for (const passthrough of passthroughs) {
+ if (seen.has(passthrough)) continue;
+ seen.add(passthrough);
+ if (passthrough === 'children') {
+ candidates.push(...collectCallSiteCandidates(ast, enclosing.name));
+ } else if (componentDestructuresProp(enclosing.fn, passthrough)) {
+ candidates.push(...collectPropCallSiteCandidates(ast, enclosing.name, passthrough));
+ }
+ }
}
}
if (candidates.length === 0) {