Skip to content

feat: design mode click-to-select widgets in dashboard preview#798

Merged
hotlong merged 5 commits intomainfrom
copilot/enable-widget-selection-preview
Feb 23, 2026
Merged

feat: design mode click-to-select widgets in dashboard preview#798
hotlong merged 5 commits intomainfrom
copilot/enable-widget-selection-preview

Conversation

Copy link
Copy Markdown
Contributor

Copilot AI commented Feb 23, 2026

  • Add selectedWidgetId, onWidgetClick, and designMode props to DashboardRenderer
  • Implement widget click-to-select with highlight in DashboardRenderer (primary ring, light/dark theme, a11y)
  • Add click-on-empty-space to deselect
  • Add keyboard navigation (Tab/Shift+Tab, Arrow keys, Enter/Space, Escape) for widget selection
  • Add selectedWidgetId and onWidgetSelect props to DashboardEditor for external control
  • Update DashboardView to sync selection between DashboardRenderer and DashboardEditor
  • Fix stale closure bugs in addWidget/removeWidget (missing setSelectedWidgetId in useCallback deps)
  • Write 15 tests for DashboardRenderer design mode (widget selection, keyboard nav, a11y, deselection)
  • Write 9 tests for DashboardEditor external selection control (including live preview path, add/remove sync)
  • Write 8 integration tests for DashboardView end-to-end selection sync (preview→editor→edit→preview→save, drawer close clears selection)
  • Run all tests — 159/159 pass across 16 test files
  • Update ROADMAP.md with Phase 6 — Design Mode Preview Click-to-Select
  • Run code review and address feedback
  • Run CodeQL security check — 0 alerts
Original prompt

This section details on the original issue you should resolve

<issue_title>支持仪表盘设计模式下:点击左侧预览区组件后,右侧面板联动显示属性并可实时预览修改效果</issue_title>
<issue_description>## 背景
目前在仪表盘设计模式(Dashboard Editor 或 DashboardView + DesignDrawer)中,属性面板的变更可实时预览,但只能通过左侧列表或卡片区选择 widget,还无法像 Airtable 那样直接在主预览区点击任意组件进行选中、配置联动(所见即所得)。

需求说明

  1. 主预览区(DashboardRenderer)所有widget都可以被鼠标直接点击选中

    • 鼠标点击任意widget(如图表、KPI卡片)时:
      • 在主预览区为选中的组件显示高亮边框、阴影或背景高亮(如蓝色描边/背景变浅,参考Airtable风格)
      • 同步将选中态传递给右侧属性面板,显示相应widget属性
  2. 右侧属性面板联动显示当前选中widget

    • 面板内容随当前选中组件变化而刷新(如切换到不同的widget属性配置)
    • 属性修改后,实时体现在主预览区(无需显式保存)
  3. 交互增强

    • 预览区点击空白处时取消选中(右侧面板合上或恢复为Dashboard整体配置面板)
    • 支持键盘←→导航选中组件(加分项,建议)
    • 连续修改属性时,预览始终同步
  4. 兼容性与无障碍

    • 高亮边框/背景需兼顾亮/暗色主题和无障碍(a11y)
    • tab/shift+tab可以切换组件选中
  5. 编码建议

    • 由DashboardRenderer外层统一管理selectedWidgetId状态,并为每个widget外包裹可点击区域
    • 向上传递onWidgetClick回调,联动DashboardEditor/DashboardView/DesignDrawer中的schema和右侧面板属性
    • 跨区域选中逻辑需避免事件冒泡干扰

验收标准

  • 在设计模式下,用户点击主预览区任意组件,右侧面板能即时显示对应属性,修改后立刻在预览区生效
  • UI高亮样式、无障碍体验良好,自动适配亮暗主题
  • 相关功能通过自动化和手动测试验证,测试覆盖核心交互路径

开发完成后务必运行 test,并更新 roadmap。

参考 Airtable 行为,如截图2。</issue_description>

Comments on the Issue (you are @copilot in this section)


✨ Let Copilot coding agent set things up for you — coding agent works faster and does higher quality work when set up for your repo.

@vercel
Copy link
Copy Markdown

vercel Bot commented Feb 23, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
objectui Ready Ready Preview, Comment Feb 23, 2026 1:34pm
objectui-demo Error Error Feb 23, 2026 1:34pm
objectui-storybook Error Error Feb 23, 2026 1:34pm

Request Review

- Add designMode, selectedWidgetId, onWidgetClick props to DashboardRenderer
- Implement widget click-to-select with primary ring highlight (a11y, light/dark)
- Add keyboard navigation (Arrow keys, Enter, Space, Escape, Tab)
- Add selectedWidgetId/onWidgetSelect to DashboardEditor for external control
- Sync selection between DashboardRenderer and DashboardEditor in DashboardView
- Add 21 new tests (15 DashboardRenderer design mode, 6 DashboardEditor external)
- Update ROADMAP.md with Phase 6 — Design Mode Preview Click-to-Select

Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com>
Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com>
Copilot AI changed the title [WIP] Add interactive widget selection in dashboard design mode feat: design mode click-to-select widgets in dashboard preview Feb 23, 2026
Copilot AI requested a review from hotlong February 23, 2026 13:08
@hotlong
Copy link
Copy Markdown
Collaborator

hotlong commented Feb 23, 2026

@copilot 仔细评估是否已经实现了完整的设计功能

…nd selection sync

- Fix stale closure in addWidget/removeWidget (missing setSelectedWidgetId dep)
- Add 8 DashboardView integration tests verifying complete selection sync flow
- Add 3 new DashboardEditor tests (live preview, add widget sync, remove widget sync)
- Update ROADMAP with verified end-to-end capabilities (32 total new tests)

Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com>
Copy link
Copy Markdown
Contributor Author

Copilot AI commented Feb 23, 2026

@copilot 仔细评估是否已经实现了完整的设计功能

经过仔细评估,发现了以下问题并已修复(commit f77f83c):

修复的 Bug:

  • addWidgetremoveWidgetuseCallback 依赖数组遗漏了 setSelectedWidgetId,在外部控制模式下可能导致闭包过期(stale closure),新增 widget 或删除 widget 时选中状态无法正确同步到预览区

补充的集成测试(+11 新测试):

  • 8 个 DashboardView 端到端集成测试,覆盖完整链路:
    • 预览区点击 widget → 编辑器面板显示对应属性 ✅
    • 编辑器点击 widget → 预览区同步选中 ✅
    • 修改属性 → 预览区实时更新 ✅
    • 点击空白区域取消选中 → 面板隐藏 ✅
    • 属性修改自动保存到后端 ✅
  • 3 个 DashboardEditor 补充测试:属性编辑触发 onChange(实时预览路径)、新增 widget 触发 onWidgetSelect、删除选中 widget 触发 onWidgetSelect(null)

功能完整性评估(全部 ✅):

  1. 预览区点击选中 widget → 高亮显示 + 右侧面板联动
  2. 属性修改实时预览(无需保存)
  3. 点击空白取消选中
  4. 键盘导航(←→↑↓、Enter/Space、Escape、Tab/Shift+Tab)
  5. 亮色/暗色主题兼容(CSS 变量)
  6. 无障碍(role、aria-selected、aria-label、focus-visible)
  7. 事件冒泡隔离(stopPropagation)
  8. 关闭抽屉自动清除选中
  9. 向后兼容(DashboardEditor 不传外部 props 仍正常工作)

共 159 测试全部通过(16 个文件)。

Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com>
@hotlong hotlong marked this pull request as ready for review February 23, 2026 13:21
Copilot AI review requested due to automatic review settings February 23, 2026 13:21
@hotlong hotlong merged commit 4f42f7b into main Feb 23, 2026
2 of 5 checks passed
Copilot stopped work on behalf of hotlong due to an error February 23, 2026 13:22
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR introduces a “design mode” for dashboard preview selection so users can click widgets in the rendered dashboard preview to select/deselect them and keep selection in sync with the right-side editor panel.

Changes:

  • Add design-mode selection props to DashboardRenderer and implement click + keyboard selection/deselection affordances.
  • Add external (controlled) widget selection props to DashboardEditor and wire selection sync in DashboardView.
  • Add unit/integration tests covering renderer/editor/view selection sync, and update ROADMAP.md.

Reviewed changes

Copilot reviewed 7 out of 7 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
packages/plugin-designer/src/tests/DashboardEditor.externalSelection.test.tsx Adds tests for externally controlled widget selection in the editor.
packages/plugin-designer/src/DashboardEditor.tsx Adds selectedWidgetId/onWidgetSelect props and controlled/uncontrolled selection behavior.
packages/plugin-dashboard/src/tests/DashboardRenderer.designMode.test.tsx Adds tests for design-mode selection, keyboard nav, and deselection behaviors.
packages/plugin-dashboard/src/DashboardRenderer.tsx Implements design-mode widget wrappers, selection styling, click-to-select/deselect, and keyboard navigation.
apps/console/src/components/DashboardView.tsx Shares selectedWidgetId state between preview renderer and editor drawer to sync selection.
apps/console/src/tests/DashboardViewSelection.test.tsx Integration tests for end-to-end selection sync between preview/editor and autosave behavior.
ROADMAP.md Documents Phase 6 “Design Mode Preview Click-to-Select”.

Comment on lines +165 to +169
const designModeProps = designMode ? {
'data-testid': `dashboard-preview-widget-${widget.id}`,
'data-widget-id': widget.id,
role: 'button' as const,
tabIndex: 0,
Copy link

Copilot AI Feb 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

DashboardWidgetSchema.id is optional, but design mode props are built unconditionally from widget.id (test id, data-widget-id, role/tabIndex, handlers). If id is missing, the element becomes focusable and labeled but can’t be selected (handleWidgetClick returns when widgetId is falsy). Consider only applying the interactive design-mode props when widget.id is present (or ensuring widgets always have stable ids before enabling selection).

Copilot uses AI. Check for mistakes.
Comment on lines +283 to 284
<div ref={ref} className={cn("flex flex-col gap-4 px-4", className)} data-user-actions={userActionsAttr} onClick={handleBackgroundClick} {...props}>
{headerSection}
Copy link

Copilot AI Feb 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

onClick={handleBackgroundClick} is set before {...props} on the mobile root container, so a consumer-provided onClick in props will override the background deselect handler and break click-to-deselect. Consider spreading props before attaching onClick, or composing the handlers so both run.

Copilot uses AI. Check for mistakes.
Comment on lines 315 to 319
}}
data-user-actions={userActionsAttr}
onClick={handleBackgroundClick}
{...props}
>
Copy link

Copilot AI Feb 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

onClick={handleBackgroundClick} is set before {...props} on the desktop grid container, so a consumer-provided onClick in props will override the background deselect handler and break click-to-deselect. Consider spreading props before attaching onClick, or composing the handlers so both run.

Copilot uses AI. Check for mistakes.
Comment on lines +83 to +92
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
onWidgetClick(widgetId ?? null);
} else if (e.key === 'ArrowRight' || e.key === 'ArrowDown') {
e.preventDefault();
const next = index + 1 < widgets.length ? widgets[index + 1] : null;
if (next?.id) onWidgetClick(next.id);
} else if (e.key === 'ArrowLeft' || e.key === 'ArrowUp') {
e.preventDefault();
const prev = index - 1 >= 0 ? widgets[index - 1] : null;
Copy link

Copilot AI Feb 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Arrow-key navigation uses schema.widgets[index ± 1], but index comes from the render loop and is relative to the list being mapped. In the mobile layout, widgets are mapped from filtered subsets (metrics vs others), so index won’t match the position in schema.widgets and navigation can jump to the wrong widget. Consider deriving the current index from widgetId (e.g., findIndex) before computing next/prev.

Suggested change
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
onWidgetClick(widgetId ?? null);
} else if (e.key === 'ArrowRight' || e.key === 'ArrowDown') {
e.preventDefault();
const next = index + 1 < widgets.length ? widgets[index + 1] : null;
if (next?.id) onWidgetClick(next.id);
} else if (e.key === 'ArrowLeft' || e.key === 'ArrowUp') {
e.preventDefault();
const prev = index - 1 >= 0 ? widgets[index - 1] : null;
// Derive the position from widgetId to avoid relying on subset indices
const currentIndex =
widgetId != null ? widgets.findIndex((w) => w.id === widgetId) : -1;
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
onWidgetClick(widgetId ?? null);
} else if (e.key === 'ArrowRight' || e.key === 'ArrowDown') {
e.preventDefault();
if (currentIndex === -1) return;
const nextIndex = currentIndex + 1;
const next = nextIndex < widgets.length ? widgets[nextIndex] : null;
if (next?.id) onWidgetClick(next.id);
} else if (e.key === 'ArrowLeft' || e.key === 'ArrowUp') {
e.preventDefault();
if (currentIndex === -1) return;
const prevIndex = currentIndex - 1;
const prev = prevIndex >= 0 ? widgets[prevIndex] : null;

Copilot uses AI. Check for mistakes.
'data-widget-id': widget.id,
role: 'button' as const,
tabIndex: 0,
'aria-selected': isSelected,
Copy link

Copilot AI Feb 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Design mode renders widgets with role="button" but also sets aria-selected. aria-selected isn’t valid for the button role and may be ignored/flagged by a11y tooling. Consider using a semantic <button> element, or switch to aria-pressed, or adopt listbox/option roles if you want to keep aria-selected.

Suggested change
'aria-selected': isSelected,
'aria-pressed': isSelected,

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

支持仪表盘设计模式下:点击左侧预览区组件后,右侧面板联动显示属性并可实时预览修改效果

3 participants