Skip to content

Commit 26d4d7f

Browse files
authored
Merge pull request #815 from objectstack-ai/copilot/investigate-left-pane-issue
2 parents 992154f + c813732 commit 26d4d7f

File tree

3 files changed

+162
-6
lines changed

3 files changed

+162
-6
lines changed

ROADMAP.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -389,6 +389,16 @@ ObjectUI is a universal Server-Driven UI (SDUI) engine built on React + Tailwind
389389
- [x] `WidgetConfigPanel` — added `headerExtra` prop for custom header actions
390390
- [x] Update 21 integration tests (10 DashboardDesignInteraction + 11 DashboardViewSelection) to verify inline config panel pattern, widget deletion, live preview sync
391391

392+
**Phase 9 — Design Mode Widget Selection Click-Through Fix:**
393+
- [x] Fix: Widget content (charts, tables via `SchemaRenderer`) intercepted click events, preventing selection in edit mode
394+
- [x] Defense layer 1: `pointer-events-none` on `SchemaRenderer` content wrappers disables chart/table hover and tooltip interactivity in design mode
395+
- [x] Defense layer 2: Transparent click-capture overlay (`absolute inset-0 z-10`) renders on top of widget content in design mode — guarantees click reaches widget handler even if SVG children override `pointer-events`
396+
- [x] Self-contained (metric) widgets: both `pointer-events-none` on SchemaRenderer + overlay inside `relative` wrapper
397+
- [x] Card-based (chart/table) widgets: `pointer-events-none` on `CardContent` inner wrapper + overlay inside `relative` Card
398+
- [x] No impact on non-design mode — widgets remain fully interactive when not editing
399+
- [x] Updated SchemaRenderer mock to forward `className` and include interactive child button for more realistic testing
400+
- [x] Add 9 new Vitest tests: pointer-events-none presence/absence, overlay presence/absence, relative positioning, click-to-select on Card-based widgets
401+
392402
### P1.11 Console — Schema-Driven View Config Panel Migration
393403

394404
> Migrated the Console ViewConfigPanel from imperative implementation (~1655 lines) to Schema-Driven architecture using `ConfigPanelRenderer` + `useConfigDraft` + `ConfigPanelSchema`, reducing to ~170 lines declarative wrapper + schema factory.

packages/plugin-dashboard/src/DashboardRenderer.tsx

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -187,14 +187,15 @@ export const DashboardRenderer = forwardRef<HTMLDivElement, DashboardRendererPro
187187
return (
188188
<div
189189
key={widgetKey}
190-
className={cn("h-full w-full", selectionClasses)}
190+
className={cn("h-full w-full", designMode && "relative", selectionClasses)}
191191
style={!isMobile && widget.layout ? {
192192
gridColumn: `span ${widget.layout.w}`,
193193
gridRow: `span ${widget.layout.h}`
194194
}: undefined}
195195
{...designModeProps}
196196
>
197-
<SchemaRenderer schema={componentSchema} className="h-full w-full" />
197+
<SchemaRenderer schema={componentSchema} className={cn("h-full w-full", designMode && "pointer-events-none")} />
198+
{designMode && <div className="absolute inset-0 z-10" aria-hidden="true" data-testid="widget-click-overlay" />}
198199
</div>
199200
);
200201
}
@@ -206,6 +207,7 @@ export const DashboardRenderer = forwardRef<HTMLDivElement, DashboardRendererPro
206207
"overflow-hidden border-border/50 shadow-sm transition-all hover:shadow-md",
207208
"bg-card/50 backdrop-blur-sm",
208209
forceMobileFullWidth && "w-full",
210+
designMode && "relative",
209211
selectionClasses
210212
)}
211213
style={!isMobile && widget.layout ? {
@@ -222,10 +224,11 @@ export const DashboardRenderer = forwardRef<HTMLDivElement, DashboardRendererPro
222224
</CardHeader>
223225
)}
224226
<CardContent className="p-0">
225-
<div className={cn("h-full w-full", "p-3 sm:p-4 md:p-6")}>
227+
<div className={cn("h-full w-full", "p-3 sm:p-4 md:p-6", designMode && "pointer-events-none")}>
226228
<SchemaRenderer schema={componentSchema} />
227229
</div>
228230
</CardContent>
231+
{designMode && <div className="absolute inset-0 z-10" aria-hidden="true" data-testid="widget-click-overlay" />}
229232
</Card>
230233
);
231234
};

packages/plugin-dashboard/src/__tests__/DashboardRenderer.designMode.test.tsx

Lines changed: 146 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,15 @@ import { render, screen, fireEvent } from '@testing-library/react';
33
import { DashboardRenderer } from '../DashboardRenderer';
44
import type { DashboardSchema } from '@object-ui/types';
55

6-
// Mock SchemaRenderer to avoid pulling in the full renderer tree
6+
// Mock SchemaRenderer to avoid pulling in the full renderer tree.
7+
// Forwards className and includes an interactive child to simulate real chart content.
78
vi.mock('@object-ui/react', () => ({
8-
SchemaRenderer: ({ schema }: { schema: any }) => (
9-
<div data-testid="schema-renderer">{schema?.type ?? 'unknown'}</div>
9+
SchemaRenderer: ({ schema, className }: { schema: any; className?: string }) => (
10+
<div data-testid="schema-renderer" className={className}>
11+
<button data-testid={`interactive-child-${schema?.type ?? 'unknown'}`}>
12+
{schema?.type ?? 'unknown'}
13+
</button>
14+
</div>
1015
),
1116
}));
1217

@@ -229,6 +234,144 @@ describe('DashboardRenderer design mode', () => {
229234
});
230235
});
231236

237+
describe('Content pointer-events in design mode', () => {
238+
it('should apply pointer-events-none to widget content in design mode', () => {
239+
render(
240+
<DashboardRenderer
241+
schema={DASHBOARD_WITH_WIDGETS}
242+
designMode
243+
selectedWidgetId={null}
244+
onWidgetClick={vi.fn()}
245+
/>,
246+
);
247+
248+
// Card widget (bar chart) — content wrapper should have pointer-events-none
249+
const barWidget = screen.getByTestId('dashboard-preview-widget-w2');
250+
const contentWrapper = barWidget.querySelector('.pointer-events-none');
251+
expect(contentWrapper).toBeInTheDocument();
252+
});
253+
254+
it('should apply pointer-events-none to self-contained (metric) widget content', () => {
255+
render(
256+
<DashboardRenderer
257+
schema={DASHBOARD_WITH_WIDGETS}
258+
designMode
259+
selectedWidgetId={null}
260+
onWidgetClick={vi.fn()}
261+
/>,
262+
);
263+
264+
// Metric widget — SchemaRenderer receives pointer-events-none className
265+
const metricWidget = screen.getByTestId('dashboard-preview-widget-w1');
266+
const contentWrapper = metricWidget.querySelector('.pointer-events-none');
267+
expect(contentWrapper).toBeInTheDocument();
268+
});
269+
270+
it('should NOT apply pointer-events-none when not in design mode', () => {
271+
const { container } = render(<DashboardRenderer schema={DASHBOARD_WITH_WIDGETS} />);
272+
273+
// No element should have pointer-events-none class
274+
expect(container.querySelector('.pointer-events-none')).not.toBeInTheDocument();
275+
});
276+
277+
it('should still call onWidgetClick when clicking on Card-based widget content area', () => {
278+
const onWidgetClick = vi.fn();
279+
render(
280+
<DashboardRenderer
281+
schema={DASHBOARD_WITH_WIDGETS}
282+
designMode
283+
selectedWidgetId={null}
284+
onWidgetClick={onWidgetClick}
285+
/>,
286+
);
287+
288+
// Click on the bar chart widget (Card-based)
289+
fireEvent.click(screen.getByTestId('dashboard-preview-widget-w2'));
290+
expect(onWidgetClick).toHaveBeenCalledWith('w2');
291+
});
292+
293+
it('should still call onWidgetClick when clicking on table widget', () => {
294+
const onWidgetClick = vi.fn();
295+
render(
296+
<DashboardRenderer
297+
schema={DASHBOARD_WITH_WIDGETS}
298+
designMode
299+
selectedWidgetId={null}
300+
onWidgetClick={onWidgetClick}
301+
/>,
302+
);
303+
304+
// Click on the table widget (Card-based)
305+
fireEvent.click(screen.getByTestId('dashboard-preview-widget-w3'));
306+
expect(onWidgetClick).toHaveBeenCalledWith('w3');
307+
});
308+
});
309+
310+
describe('Click-capture overlay in design mode', () => {
311+
it('should render a click-capture overlay on Card-based widgets in design mode', () => {
312+
render(
313+
<DashboardRenderer
314+
schema={DASHBOARD_WITH_WIDGETS}
315+
designMode
316+
selectedWidgetId={null}
317+
onWidgetClick={vi.fn()}
318+
/>,
319+
);
320+
321+
// Card widget (bar chart) — should have an absolute overlay div
322+
const barWidget = screen.getByTestId('dashboard-preview-widget-w2');
323+
const overlay = barWidget.querySelector('[data-testid="widget-click-overlay"]');
324+
expect(overlay).toBeInTheDocument();
325+
expect(overlay?.className).toContain('absolute');
326+
expect(overlay?.className).toContain('inset-0');
327+
expect(overlay?.className).toContain('z-10');
328+
});
329+
330+
it('should render a click-capture overlay on self-contained widgets in design mode', () => {
331+
render(
332+
<DashboardRenderer
333+
schema={DASHBOARD_WITH_WIDGETS}
334+
designMode
335+
selectedWidgetId={null}
336+
onWidgetClick={vi.fn()}
337+
/>,
338+
);
339+
340+
// Metric widget — should have an absolute overlay div
341+
const metricWidget = screen.getByTestId('dashboard-preview-widget-w1');
342+
const overlay = metricWidget.querySelector('[data-testid="widget-click-overlay"]');
343+
expect(overlay).toBeInTheDocument();
344+
expect(overlay?.className).toContain('absolute');
345+
expect(overlay?.className).toContain('inset-0');
346+
expect(overlay?.className).toContain('z-10');
347+
});
348+
349+
it('should NOT render overlays when not in design mode', () => {
350+
const { container } = render(<DashboardRenderer schema={DASHBOARD_WITH_WIDGETS} />);
351+
352+
expect(container.querySelector('[data-testid="widget-click-overlay"]')).not.toBeInTheDocument();
353+
});
354+
355+
it('should apply relative positioning to widget container in design mode', () => {
356+
render(
357+
<DashboardRenderer
358+
schema={DASHBOARD_WITH_WIDGETS}
359+
designMode
360+
selectedWidgetId={null}
361+
onWidgetClick={vi.fn()}
362+
/>,
363+
);
364+
365+
// Card widget should have relative for overlay positioning
366+
const barWidget = screen.getByTestId('dashboard-preview-widget-w2');
367+
expect(barWidget.className).toContain('relative');
368+
369+
// Metric widget should have relative for overlay positioning
370+
const metricWidget = screen.getByTestId('dashboard-preview-widget-w1');
371+
expect(metricWidget.className).toContain('relative');
372+
});
373+
});
374+
232375
describe('Non-design mode behavior', () => {
233376
it('should not add design mode attributes when designMode is off', () => {
234377
const { container } = render(<DashboardRenderer schema={DASHBOARD_WITH_WIDGETS} />);

0 commit comments

Comments
 (0)