Skip to content

[8220] Accessibility: Consider keyboard shortcut disabling #4882

Open
jvega190 wants to merge 10 commits into
craftercms:developfrom
jvega190:enhancement/8220
Open

[8220] Accessibility: Consider keyboard shortcut disabling #4882
jvega190 wants to merge 10 commits into
craftercms:developfrom
jvega190:enhancement/8220

Conversation

@jvega190

@jvega190 jvega190 commented Jun 8, 2026

Copy link
Copy Markdown
Member

craftercms/craftercms#8220

Summary by CodeRabbit

  • New Features
    • Added a “Keyboard shortcuts” toggle in the Preview Settings Panel.
    • Keyboard shortcut preference is saved per user, restored on preview load, and applied to the preview.
  • Bug Fixes
    • Keyboard shortcuts no longer trigger while any dialogs are open or a dialog stack has entries.
  • Refactor
    • Updated widget dialog opening/updates to use the shared dialog stack for consistent behavior.
  • Chores
    • Improved widget dialog minimize/restore behavior and ensured preference clearing removes the stored keyboard-shortcuts setting.

@coderabbitai

coderabbitai Bot commented Jun 9, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Warning

Review limit reached

@jvega190, we couldn't start this review because you've reached your PR review rate limit.

More reviews will be available in 58 minutes and 50 seconds. Learn how PR review limits work.

Your organization has run out of usage credits. Purchase more credits in the billing tab to continue.

⌛ How to resolve this issue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based credits.

🚦 How do rate limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: c720a92c-5dd5-4b45-8aa1-d2184122e852

📥 Commits

Reviewing files that changed from the base of the PR and between 9990ce9 and cab9dde.

📒 Files selected for processing (12)
  • ui/app/src/components/AccountManagement/utils.tsx
  • ui/app/src/components/KeyboardShortcutsSwitch/KeyboardShortcutsSwitch.tsx
  • ui/app/src/components/KeyboardShortcutsSwitch/index.ts
  • ui/app/src/components/PreviewConcierge/PreviewConcierge.tsx
  • ui/app/src/components/PreviewSettingsPanel/PreviewSettingsPanel.tsx
  • ui/app/src/components/ToolsPanelEmbeddedAppViewButton/ToolsPanelEmbeddedAppViewButton.tsx
  • ui/app/src/components/WidgetDialogIconButton/WidgetDialogIconButton.tsx
  • ui/app/src/models/GlobalState.ts
  • ui/app/src/state/actions/preview.ts
  • ui/app/src/state/epics/mappedDialogs.ts
  • ui/app/src/state/reducers/preview.ts
  • ui/app/src/utils/state.ts

Walkthrough

The PR adds a per-user keyboard shortcuts enable/disable preference stored in localStorage and Redux preview state, with a new switch in PreviewSettingsPanel and shortcut blocking in PreviewConcierge. It also moves widget dialog opening and updating to the Redux dialog stack and enables widget dialogs to minimize.

Changes

Keyboard Shortcuts Toggle

Layer / File(s) Summary
State contracts, actions, reducer, and storage
ui/app/src/models/GlobalState.ts, ui/app/src/state/actions/preview.ts, ui/app/src/state/reducers/preview.ts, ui/app/src/utils/state.ts
Adds keyboardShortcutsEnabled to preview state, extends initPreviewConfig, adds setKeyboardShortcutsEnabled, wires reducer handling, and adds per-user localStorage helpers.
KeyboardShortcutsSwitch component and export
ui/app/src/components/KeyboardShortcutsSwitch/KeyboardShortcutsSwitch.tsx, ui/app/src/components/KeyboardShortcutsSwitch/index.ts
Adds the new switch component, which reads preview state, persists toggles, dispatches the action, and renders localized tooltip/switch UI.
PreviewConcierge initialization and shortcut guard
ui/app/src/components/PreviewConcierge/PreviewConcierge.tsx
Loads the stored preference during preview init, tracks dialog and shortcut-enabled state, and exits shortcut handling when dialogs are open or shortcuts are disabled.
Preview settings row and preference clearing
ui/app/src/components/PreviewSettingsPanel/PreviewSettingsPanel.tsx, ui/app/src/components/AccountManagement/utils.tsx
Adds the keyboard shortcuts row to preview settings and clears the stored preference from the Miscellaneous reset flow.

Widget Dialog Redux Stack Refactor

Layer / File(s) Summary
showWidgetDialog can minimize
ui/app/src/state/epics/mappedDialogs.ts
Adds showWidgetDialog.type to the minimize-enabled dialog types.
ToolsPanelEmbeddedAppViewButton Redux dialog wiring
ui/app/src/components/ToolsPanelEmbeddedAppViewButton/ToolsPanelEmbeddedAppViewButton.tsx
Switches the widget dialog launcher to Redux dialog stack actions and removes inline dialog rendering.
WidgetDialogIconButton Redux dialog wiring
ui/app/src/components/WidgetDialogIconButton/WidgetDialogIconButton.tsx
Switches the icon-button launcher to Redux dialog stack actions and removes inline dialog rendering.

Sequence Diagram(s)

sequenceDiagram
  participant User
  participant PreviewSettingsPanel
  participant KeyboardShortcutsSwitch
  participant localStorage
  participant previewReducer
  participant PreviewConcierge

  User->>PreviewSettingsPanel: toggles keyboard shortcuts
  PreviewSettingsPanel->>KeyboardShortcutsSwitch: onChange(checked)
  KeyboardShortcutsSwitch->>localStorage: setStoredEnabledKeyboardShortcutsState(checked, username)
  KeyboardShortcutsSwitch->>previewReducer: setKeyboardShortcutsEnabled({ enabled: checked })
  previewReducer-->>previewReducer: state.keyboardShortcutsEnabled = checked

  User->>PreviewConcierge: keypress event
  PreviewConcierge->>PreviewConcierge: check dialogs and keyboardShortcutsEnabled
  alt shortcuts disabled or dialog open
    PreviewConcierge-->>User: no shortcut action
  else shortcuts enabled
    PreviewConcierge-->>User: handle shortcut
  end
Loading
sequenceDiagram
  participant User
  participant ToolsPanelEmbeddedAppViewButton
  participant WidgetDialogIconButton
  participant dialogStack
  participant ReduxStore

  User->>ToolsPanelEmbeddedAppViewButton: open embedded app
  ToolsPanelEmbeddedAppViewButton->>ReduxStore: read dialogStack.byId[widgetDialogId]
  alt dialog exists
    ToolsPanelEmbeddedAppViewButton->>dialogStack: updateDialogState(...)
  else no dialog exists
    ToolsPanelEmbeddedAppViewButton->>dialogStack: pushDialog(...)
  end

  User->>WidgetDialogIconButton: open widget dialog
  WidgetDialogIconButton->>ReduxStore: read dialogStack.byId[widget.id]
  alt dialog exists
    WidgetDialogIconButton->>dialogStack: updateDialogState(...)
  else no dialog exists
    WidgetDialogIconButton->>dialogStack: pushDialog(...)
  end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

  • craftercms/studio-ui#4886: Shares the same keyboard-shortcuts preference plumbing across KeyboardShortcutsSwitch, PreviewConcierge, preview actions/reducer, and localStorage helpers.

Suggested reviewers

  • rart
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title matches the PR's accessibility focus on letting users disable keyboard shortcuts.
Description check ✅ Passed The description satisfies the template by providing the required ticket reference for issue 8220.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@jvega190

jvega190 commented Jun 9, 2026

Copy link
Copy Markdown
Member Author

@coderabbitai can you check if everything looks good? Please give it a final review before I hand over for human review

@coderabbitai

coderabbitai Bot commented Jun 9, 2026

Copy link
Copy Markdown
Contributor

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
echo "=== PR Details ==="
gh pr view 4882 --repo craftercms/studio-ui --json title,body,state,additions,deletions,changedFiles,baseRefName,headRefName,labels,reviewRequests

echo ""
echo "=== Changed Files ==="
gh pr diff 4882 --repo craftercms/studio-ui --name-only

echo ""
echo "=== PR Diff ==="
gh pr diff 4882 --repo craftercms/studio-ui

Length of output: 13825



@jvega190 The implementation looks solid overall — the feature is well-structured, follows existing patterns in the codebase, and correctly handles persistence, Redux state, and cleanup. A few minor items worth addressing before human review:


1. Import inconsistency in KeyboardShortcutsSwitch.tsx

Switch is imported from the MUI barrel (@mui/material) while the other MUI imports use direct sub-paths. Should be consistent:

// Current (inconsistent):
import Tooltip from '`@mui/material/Tooltip`';
import type { SwitchProps } from '`@mui/material/Switch`';
import { Switch } from '`@mui/material`';            // ← barrel import

// Suggested (consistent):
import Tooltip from '`@mui/material/Tooltip`';
import Switch from '`@mui/material/Switch`';
import type { SwitchProps } from '`@mui/material/Switch`';

2. Redundant ?? true in the reducer / misleading action payload type

getStoredEnabledKeyboardShortcutsState always returns boolean (it returns true as default when the key is absent — never null). This makes storedEnabledKeyboardShortcuts: boolean | null in the action and the ?? true fallback in the reducer redundant:

// actions/preview.ts
storedEnabledKeyboardShortcuts: boolean;   // not `boolean | null`

// reducers/preview.ts  
// `?? true` is dead code since the getter never returns null
state.enableKeyboardShortcuts = payload.storedEnabledKeyboardShortcuts;

3. Accessibility of the Switch input itself ⚠️

Since this PR is specifically about accessibility, the Switch inside KeyboardShortcutsSwitch lacks an explicit accessible label for screen readers. The Tooltip title is hover-only and the nearby ListItemText is not programmatically associated with the switch input. Consider adding inputProps:

<Switch
  {...props}
  checked={shortcutsEnabled}
  onChange={onChange}
  inputProps={{ 'aria-label': 'Toggle keyboard shortcuts' }}
/>

4. Helper text could be more descriptive

"Enable/disable keyboard shortcuts" is generic. Something like "When disabled, keyboard shortcuts won't trigger preview actions" would give users more context about what this controls.


Summary: The core logic (state, persistence, early-return guard in onShortCutKeypress, cleanup on preference reset) all look correct. The items above are mostly minor/style, except the aria-label which is meaningful given the accessibility theme of the PR.

@jvega190

jvega190 commented Jun 9, 2026

Copy link
Copy Markdown
Member Author

@coderabbitai can you check if everything looks good? Please give it a final review before I hand over for human review

@coderabbitai

coderabbitai Bot commented Jun 9, 2026

Copy link
Copy Markdown
Contributor

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
echo "=== PR Details ==="
gh pr view 4882 --repo craftercms/studio-ui --json title,body,state,additions,deletions,changedFiles,baseRefName,headRefName

echo ""
echo "=== Changed Files ==="
gh pr diff 4882 --repo craftercms/studio-ui --name-only

echo ""
echo "=== Full PR Diff ==="
gh pr diff 4882 --repo craftercms/studio-ui

Length of output: 13280



@jvega190 All three significant issues from the previous review have been addressed — great work! Here's the final status:

Issue Status
Import inconsistency (Switch from barrel vs sub-path) ✅ Fixed — now uses @mui/material/Switch
Redundant ?? true / misleading boolean | null type ✅ Fixed — action type is boolean, no fallback in reducer
Missing accessible label on Switch ✅ Fixed — uses slotProps.input['aria-label'] (modern MUI approach, actually better than the inputProps I suggested since inputProps is deprecated in MUI v6)
Generic helper text ⬜ Optional — still "Enable/disable keyboard shortcuts", minor style preference

A couple of additional things I noticed while scanning the updated diff that look correct:

  • PreviewConcierge.tsxstack and keyboardShortcutsEnabled are properly added to upToDateRefs before the early-return guard, so there are no stale closure issues. ✅
  • utils/state.tsJSON.stringify(boolean) for storage and value === 'true' for reading are consistent. ✅
  • AccountManagement/utils.tsx — Preference reset correctly includes removeStoredEnabledKeyboardShortcutsState. ✅

The implementation is solid and ready for human review. 🐇

@jvega190 jvega190 marked this pull request as ready for review June 9, 2026 15:27
@jvega190 jvega190 requested a review from rart as a code owner June 9, 2026 15:27

@rart rart left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Apart from this, I think there's a few instances (inputs, inside dialogs) where the key event is reaching the shortcut instead of stopping at the current interaction target. Please double check.

…lEmbeddedAppViewButton and WidgetDialogIconButton to use new dialogs system
@rart

rart commented Jun 25, 2026

Copy link
Copy Markdown
Member

@coderabbitai I don't see a review of 792b9b9. Please review this PR and report findings

@rart rart left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Conflicts

@coderabbitai

coderabbitai Bot commented Jun 25, 2026

Copy link
Copy Markdown
Contributor
🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

repo="craftercms/studio-ui"
pr="4882"
commit="792b9b98ab0d9d1a9554acb36d11db19a4bcc5af"

echo "=== PR metadata ==="
gh pr view "$pr" --repo "$repo" --json title,body,files,commits \
  --jq '{title, files: [.files[].path], commits: [.commits[] | {oid, messageHeadline}] }'

echo
echo "=== Commit metadata ==="
gh api "repos/$repo/commits/$commit" --jq '{sha: .sha, files: [.files[] | {filename, status, additions, deletions, changes}]}'

echo
echo "=== Commit patch ==="
gh api "repos/$repo/commits/$commit" --jq '.files[] | "FILE: \(.filename)\n\(.patch // "[no patch]")\n---"'

echo
echo "=== PR diff names ==="
gh pr diff "$pr" --repo "$repo" --name-only

echo
echo "=== Outline: ui/app/src/components/ToolsPanelEmbeddedAppViewButton/ToolsPanelEmbeddedAppViewButton.tsx ==="
ast-grep outline ui/app/src/components/ToolsPanelEmbeddedAppViewButton/ToolsPanelEmbeddedAppViewButton.tsx --view expanded || true

echo
echo "=== Outline: ui/app/src/components/WidgetDialogIconButton/WidgetDialogIconButton.tsx ==="
ast-grep outline ui/app/src/components/WidgetDialogIconButton/WidgetDialogIconButton.tsx --view expanded || true

echo
echo "=== Outline: ui/app/src/state/epics/mappedDialogs.ts ==="
ast-grep outline ui/app/src/state/epics/mappedDialogs.ts --view expanded || true

echo
echo "=== Outline: ui/app/src/state/actions/dialogs.ts ==="
ast-grep outline ui/app/src/state/actions/dialogs.ts --view expanded || true

echo
echo "=== Snippet: ToolsPanelEmbeddedAppViewButton.tsx ==="
sed -n '1,260p' ui/app/src/components/ToolsPanelEmbeddedAppViewButton/ToolsPanelEmbeddedAppViewButton.tsx

echo
echo "=== Snippet: WidgetDialogIconButton.tsx ==="
sed -n '1,260p' ui/app/src/components/WidgetDialogIconButton/WidgetDialogIconButton.tsx

echo
echo "=== Snippet: mappedDialogs.ts ==="
sed -n '1,260p' ui/app/src/state/epics/mappedDialogs.ts

echo
echo "=== Search: generateDialogId, showWidgetDialog, updateWidgetDialog ==="
rg -n -C3 'generateDialogId|showWidgetDialog|updateWidgetDialog' ui/app/src

echo
echo "=== Search: allowMinimizeDialogs ==="
rg -n -C3 'allowMinimizeDialogs' ui/app/src

Length of output: 58880


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== Locate WidgetDialog-related files ==="
fd -i 'WidgetDialog*' ui/app/src/components ui/app/src/models ui/app/src/state || true

echo
echo "=== Outline: ui/app/src/components/WidgetDialog/WidgetDialog.tsx ==="
ast-grep outline ui/app/src/components/WidgetDialog/WidgetDialog.tsx --view expanded || true

echo
echo "=== Snippet: ui/app/src/components/WidgetDialog/WidgetDialog.tsx ==="
sed -n '1,260p' ui/app/src/components/WidgetDialog/WidgetDialog.tsx || true

echo
echo "=== Search: WidgetDialogStateProps ==="
rg -n -C3 'WidgetDialogStateProps' ui/app/src

echo
echo "=== Search: craftercms.components.WidgetDialog ==="
rg -n -C3 'craftercms\.components\.WidgetDialog|pushDialog\(|dialogStack' ui/app/src/components ui/app/src/state

echo
echo "=== Search: useWithPendingChangesCloseRequest / useEnhancedDialogState around widget dialog usages ==="
rg -n -C4 'useWithPendingChangesCloseRequest|useEnhancedDialogState|WidgetDialog' ui/app/src/components

echo
echo "=== Search: interface/type EnhancedDialogProps ==="
rg -n -C3 'interface EnhancedDialogProps|type EnhancedDialogProps' ui/app/src/components ui/app/src/models ui/app/src/state

echo
echo "=== Search: state.dialogs shape / open checks ==="
rg -n -C3 'state\.dialogs|dialogs\).+open|dialog\.open' ui/app/src

Length of output: 196238



@rart I reviewed #4882, including commit 792b9b9, and found one substantive regression risk.

Finding

  • Widget dialogs were changed from per-launcher instances to a single global dialog id.
    In ui/app/src/components/ToolsPanelEmbeddedAppViewButton/ToolsPanelEmbeddedAppViewButton.tsx around Line 36 and ui/app/src/components/WidgetDialogIconButton/WidgetDialogIconButton.tsx around Line 40, both launchers now use:

    • generateDialogId(showWidgetDialog.type)
    • store.getState().dialogStack.byId[widgetDialogId]
    • showWidgetDialog(...) / updateWidgetDialog(...)

    Since ui/app/src/utils/dialogs.ts turns SHOW_WIDGET_DIALOG into the fixed id WIDGET_DIALOG, and ui/app/src/state/epics/mappedDialogs.ts around Line 181 uses that same generated id for both push/update paths, all widget launches now target the same dialog-stack entry.

    That means if widget A is already open (or minimized) and the user launches widget B, the existing dialog is updated in place instead of opening an independent dialog. Before this commit, both components rendered their own local <WidgetDialog />, so dialog ownership was per component instance and multiple widget dialogs could coexist independently.

Why this matters

  • This changes user-visible behavior beyond “make widget dialogs participate in the global stack.”
  • It also means a minimized widget dialog can be silently repurposed for a different widget, which is surprising UX.

Suggested direction

  • Use a dialog id that is specific to the widget or launcher instance, and plumb that id through the widget dialog actions/update path.
  • If singleton behavior is actually intended, I’d recommend documenting that explicitly in the PR because it is a behavior change from the previous implementation.

Other than that, the PreviewConcierge.tsx shortcut guard looked reasonable in the updated commit.

✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🧹 Nitpick comments (1)
ui/app/src/components/KeyboardShortcutsSwitch/KeyboardShortcutsSwitch.tsx (1)

49-58: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Optional: consumer-supplied onChange is silently overridden.

Since {...props} is spread before onChange={onChange}, any onChange passed by a parent is dropped. The only current caller passes just edge="end", so this isn't a live bug, but composing the handlers would make the component safer against future reuse.

♻️ Compose handlers
-	const onChange = (e: ChangeEvent<HTMLInputElement>) => {
+	const onChange = (e: ChangeEvent<HTMLInputElement>) => {
 		setStoredEnabledKeyboardShortcutsState(e.target.checked, username);
 		dispatch(enableKeyboardShortcutsAction({ enabled: e.target.checked }));
+		props.onChange?.(e, e.target.checked);
 	};
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@ui/app/src/components/KeyboardShortcutsSwitch/KeyboardShortcutsSwitch.tsx`
around lines 49 - 58, The KeyboardShortcutsSwitch component is overriding any
consumer-provided onChange by setting its own handler after spreading props.
Update KeyboardShortcutsSwitch so the Switch composes the internal onChange with
any onChange from props instead of replacing it, preserving parent behavior
while still toggling shortcuts. Use the existing KeyboardShortcutsSwitch and
onChange wiring around the Switch element to locate the fix.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In
`@ui/app/src/components/ToolsPanelEmbeddedAppViewButton/ToolsPanelEmbeddedAppViewButton.tsx`:
- Around line 36-50: The embedded app launcher is reusing a single dialog id
from generateDialogId(showWidgetDialog.type), so openEmbeddedApp can only ever
target one shared dialog entry. Update the dialog id to include a
launcher/widget-specific key such as widget.uiKey or widget.id, and pass that id
through both showWidgetDialog and updateWidgetDialog. Also make sure the dialog
action types/handlers that consume these actions honor the supplied id so each
widget opens and restores independently.

In `@ui/app/src/components/WidgetDialogIconButton/WidgetDialogIconButton.tsx`:
- Around line 40-54: The WidgetDialogIconButton is still using the shared
type-level WIDGET_DIALOG id, which can target the wrong dialog in the Redux
stack. Update openEmbeddedApp to generate a per-widget dialog id from the
widget/launcher instance, then pass that same id through both showWidgetDialog
and updateWidgetDialog so the lookup in dialogStack.byId and the dispatches stay
scoped to the correct widget dialog.

In `@ui/app/src/state/actions/preview.ts`:
- Around line 451-452: The action name in preview.ts is semantically misleading
because enableKeyboardShortcuts implies only turning shortcuts on, while the
payload on createAction<{ enabled: boolean }> supports both states. Rename
enableKeyboardShortcuts to setKeyboardShortcutsEnabled and update any references
to match, keeping the KEYBOARD_SHORTCUTS_TOGGLE action creator behavior
unchanged.

---

Nitpick comments:
In `@ui/app/src/components/KeyboardShortcutsSwitch/KeyboardShortcutsSwitch.tsx`:
- Around line 49-58: The KeyboardShortcutsSwitch component is overriding any
consumer-provided onChange by setting its own handler after spreading props.
Update KeyboardShortcutsSwitch so the Switch composes the internal onChange with
any onChange from props instead of replacing it, preserving parent behavior
while still toggling shortcuts. Use the existing KeyboardShortcutsSwitch and
onChange wiring around the Switch element to locate the fix.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 51647a64-0921-4c3f-b807-29480bbd4206

📥 Commits

Reviewing files that changed from the base of the PR and between c6402fd and 792b9b9.

📒 Files selected for processing (12)
  • ui/app/src/components/AccountManagement/utils.tsx
  • ui/app/src/components/KeyboardShortcutsSwitch/KeyboardShortcutsSwitch.tsx
  • ui/app/src/components/KeyboardShortcutsSwitch/index.ts
  • ui/app/src/components/PreviewConcierge/PreviewConcierge.tsx
  • ui/app/src/components/PreviewSettingsPanel/PreviewSettingsPanel.tsx
  • ui/app/src/components/ToolsPanelEmbeddedAppViewButton/ToolsPanelEmbeddedAppViewButton.tsx
  • ui/app/src/components/WidgetDialogIconButton/WidgetDialogIconButton.tsx
  • ui/app/src/models/GlobalState.ts
  • ui/app/src/state/actions/preview.ts
  • ui/app/src/state/epics/mappedDialogs.ts
  • ui/app/src/state/reducers/preview.ts
  • ui/app/src/utils/state.ts

Comment thread ui/app/src/components/WidgetDialogIconButton/WidgetDialogIconButton.tsx Outdated
Comment thread ui/app/src/state/actions/preview.ts Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick comments (2)
ui/app/src/components/ToolsPanelEmbeddedAppViewButton/ToolsPanelEmbeddedAppViewButton.tsx (1)

36-59: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Namespace the widget dialog id before using it as a global stack key.

dialogStack.byId is shared across dialog types, and the reducer uses the supplied id directly. A raw widget.id can collide with another explicit dialog id and make Line 39/43 update the wrong dialog entry. Keep the per-widget behavior, but prefix it with the WidgetDialog component id.

Suggested change
 	const title = usePossibleTranslation(props.title);
 	const dispatch = useDispatch();
 	const store = useStore<GlobalState>();
-	const widgetDialogId = widget.id;
+	const widgetDialogComponentId = createComponentId('WidgetDialog');
+	const widgetDialogId = `${widgetDialogComponentId}:${widget.id}`;
 
 	const openEmbeddedApp = () => {
 		const existing = store.getState().dialogStack.byId[widgetDialogId];
@@
 				pushDialog({
 					id: widgetDialogId,
-					component: createComponentId('WidgetDialog'),
+					component: widgetDialogComponentId,
 					allowMinimize: true,
 					props: { title, widget }
 				})
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@ui/app/src/components/ToolsPanelEmbeddedAppViewButton/ToolsPanelEmbeddedAppViewButton.tsx`
around lines 36 - 59, Namespace the dialog key used by
ToolsPanelEmbeddedAppViewButton so it cannot collide with other entries in
dialogStack.byId. Update openEmbeddedApp to use a WidgetDialog-scoped id instead
of the raw widget.id, and apply that same namespaced value consistently in both
the updateDialogState and pushDialog paths so the existing lookup and per-widget
behavior still work without affecting other dialog types.
ui/app/src/components/WidgetDialogIconButton/WidgetDialogIconButton.tsx (1)

40-63: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Namespace the widget dialog id before storing it in the shared dialog stack.

This uses widget.id directly as a global dialogStack.byId key. Prefix it with the WidgetDialog component id so this launcher cannot accidentally update a different dialog type with the same explicit id.

Suggested change
 	const title = usePossibleTranslation(props.title);
 	const dispatch = useDispatch();
 	const store = useStore<GlobalState>();
-	const widgetDialogId = widget.id;
+	const widgetDialogComponentId = createComponentId('WidgetDialog');
+	const widgetDialogId = `${widgetDialogComponentId}:${widget.id}`;
 
 	const openEmbeddedApp = () => {
 		const existing = store.getState().dialogStack.byId[widgetDialogId];
@@
 				pushDialog({
 					id: widgetDialogId,
-					component: createComponentId('WidgetDialog'),
+					component: widgetDialogComponentId,
 					allowMinimize: true,
 					props: { title, widget }
 				})
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@ui/app/src/components/WidgetDialogIconButton/WidgetDialogIconButton.tsx`
around lines 40 - 63, The dialog key is using widget.id directly in the shared
dialog stack, which can collide with other dialog types. Update openEmbeddedApp
in WidgetDialogIconButton to namespace widgetDialogId with the WidgetDialog
component id via createComponentId('WidgetDialog') before reading or writing
dialogStack.byId. Keep using that namespaced id consistently in both
updateDialogState and pushDialog so this launcher only targets its own dialog
instance.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In
`@ui/app/src/components/ToolsPanelEmbeddedAppViewButton/ToolsPanelEmbeddedAppViewButton.tsx`:
- Around line 36-59: Namespace the dialog key used by
ToolsPanelEmbeddedAppViewButton so it cannot collide with other entries in
dialogStack.byId. Update openEmbeddedApp to use a WidgetDialog-scoped id instead
of the raw widget.id, and apply that same namespaced value consistently in both
the updateDialogState and pushDialog paths so the existing lookup and per-widget
behavior still work without affecting other dialog types.

In `@ui/app/src/components/WidgetDialogIconButton/WidgetDialogIconButton.tsx`:
- Around line 40-63: The dialog key is using widget.id directly in the shared
dialog stack, which can collide with other dialog types. Update openEmbeddedApp
in WidgetDialogIconButton to namespace widgetDialogId with the WidgetDialog
component id via createComponentId('WidgetDialog') before reading or writing
dialogStack.byId. Keep using that namespaced id consistently in both
updateDialogState and pushDialog so this launcher only targets its own dialog
instance.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: e2e5c814-c17c-427f-8853-67bb71fbe733

📥 Commits

Reviewing files that changed from the base of the PR and between 792b9b9 and 59835a9.

📒 Files selected for processing (7)
  • ui/app/src/components/KeyboardShortcutsSwitch/KeyboardShortcutsSwitch.tsx
  • ui/app/src/components/PreviewConcierge/PreviewConcierge.tsx
  • ui/app/src/components/ToolsPanelEmbeddedAppViewButton/ToolsPanelEmbeddedAppViewButton.tsx
  • ui/app/src/components/WidgetDialogIconButton/WidgetDialogIconButton.tsx
  • ui/app/src/models/GlobalState.ts
  • ui/app/src/state/actions/preview.ts
  • ui/app/src/state/reducers/preview.ts
🚧 Files skipped from review as they are similar to previous changes (3)
  • ui/app/src/models/GlobalState.ts
  • ui/app/src/components/KeyboardShortcutsSwitch/KeyboardShortcutsSwitch.tsx
  • ui/app/src/components/PreviewConcierge/PreviewConcierge.tsx

…o enhancement/8220

# Conflicts:
#	ui/app/src/utils/state.ts
@jvega190

Copy link
Copy Markdown
Member Author

@coderabbitai can you check if everything looks good? Please give it a final review before I hand over for human review

@coderabbitai

coderabbitai Bot commented Jun 25, 2026

Copy link
Copy Markdown
Contributor

@jvega190 I took a final pass over #4882.

Overall, the keyboard shortcuts accessibility feature looks solid:

  • the earlier import/type/accessibility issues were addressed
  • persistence and cleanup look correct
  • the PreviewConcierge.tsx guard for disabled shortcuts looks reasonable

That said, I would not call this fully ready yet because there’s still one substantive regression risk from commit 792b9b9:

  • In ui/app/src/components/ToolsPanelEmbeddedAppViewButton/ToolsPanelEmbeddedAppViewButton.tsx
  • and ui/app/src/components/WidgetDialogIconButton/WidgetDialogIconButton.tsx

the widget dialog handling appears to have shifted from per-launcher/per-instance dialogs to a single shared dialog id. That means opening widget B can update/replace widget A’s dialog instead of allowing them to coexist independently. If that behavior change is intentional, it should be explicitly documented; otherwise I’d recommend fixing it before human review.

Bottom line: feature work looks good, but I’d address or explicitly justify the widget-dialog singleton behavior before handoff.

✅ Action performed

Full review finished.

@jvega190

Copy link
Copy Markdown
Member Author

@rart what do you think of only one widget dialog of a single component can be open at a time?
e.g. two widgets craftercms.components.SiteDashboard and craftercms.components.EmbeddedSiteTools can be open at the same time, but two instances of craftercms.components.SiteDashboard can't.

@jvega190 jvega190 requested a review from rart June 25, 2026 16:32
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.

2 participants