feat(site-builder): array editors scroll to the item picked in Layers

Wires ArrayItemFieldsEditor and FeaturesEditor up to useLayerFocus() so
clicking a virtual row in the Layers tree scrolls the matching item's
card into view in the right-hand array editor. scrollIntoView is
optional-chained on both the queried element and the method itself so
a miss or an environment without it degrades silently.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-08-09 07:21:27 -07:00
co-authored by Claude Opus 5
parent 2438777462
commit cf38fdb245
2 changed files with 42 additions and 5 deletions
@@ -1,6 +1,7 @@
import React from 'react';
import React, { useEffect, useRef } from 'react';
import { useEditor } from '@craftjs/core';
import { CollapsibleSection, ArrayPropEditor, smallInputStyle } from './shared';
import { useLayerFocus } from '../../left/LayerFocusContext';
/* ---------- Shared array-item field editor ----------
Extracted from SectionTypePanel and GenericPropsEditor, which both had a
@@ -18,7 +19,26 @@ export const ArrayItemFieldsEditor: React.FC<{ selectedId: string; propKey: stri
const sampleItem = arrayItems[0] || {};
const itemFields = typeof sampleItem === 'object' && sampleItem !== null ? Object.keys(sampleItem) : [];
// Layers panel -> array editor "scroll to this item" hookup. The outer
// <div ref={rootRef}> wraps ArrayPropEditor's rendered cards (the actual
// per-item background box lives in shared.tsx's ArrayPropEditor, which is
// also used by MediaStylePanel/FormStylePanel -- rather than touch that
// shared component for one consumer, the data-array-item tag below goes
// on the content renderItem returns, which is enough for scrollIntoView
// to bring the right card into the viewport.
const { focus } = useLayerFocus();
const rootRef = useRef<HTMLDivElement | null>(null);
useEffect(() => {
if (!focus || focus.prop !== propKey) return;
const card = rootRef.current?.querySelector(`[data-array-item="${propKey}:${focus.index}"]`);
card?.scrollIntoView?.({ block: 'nearest', behavior: 'smooth' });
// `focus.nonce` is in the dep list so clicking the SAME row twice
// re-scrolls (the request object is otherwise identical).
}, [focus?.nonce, focus?.prop, focus?.index, propKey]);
return (
<div ref={rootRef}>
<CollapsibleSection title={propKey.replace(/([A-Z])/g, ' $1').trim()}>
<ArrayPropEditor
selectedId={selectedId}
@@ -29,6 +49,7 @@ export const ArrayItemFieldsEditor: React.FC<{ selectedId: string; propKey: stri
return (
<input
type="text"
data-array-item={`${propKey}:${index}`}
value={String(item)}
onChange={(e) => {
actions.setProp(selectedId, (props: any) => {
@@ -42,7 +63,7 @@ export const ArrayItemFieldsEditor: React.FC<{ selectedId: string; propKey: stri
);
}
return (
<div style={{ display: 'flex', flexDirection: 'column', gap: 3 }}>
<div data-array-item={`${propKey}:${index}`} style={{ display: 'flex', flexDirection: 'column', gap: 3 }}>
{itemFields.map((field) => {
const fieldVal = item[field];
if (typeof fieldVal === 'boolean') {
@@ -123,5 +144,6 @@ export const ArrayItemFieldsEditor: React.FC<{ selectedId: string; propKey: stri
}
/>
</CollapsibleSection>
</div>
);
};