Skip to content

component(ContextMenuViewport): move to ui-next with theme support#6008

Open
dan-rukas wants to merge 7 commits into
OHIF:masterfrom
dan-rukas:feat/context-menu-update
Open

component(ContextMenuViewport): move to ui-next with theme support#6008
dan-rukas wants to merge 7 commits into
OHIF:masterfrom
dan-rukas:feat/context-menu-update

Conversation

@dan-rukas

@dan-rukas dan-rukas commented May 8, 2026

Copy link
Copy Markdown
Member

Context

Moves the viewport context menu from @ohif/ui to @ohif/ui-next as ContextMenuViewport. The legacy component used hardcoded colors that don't respond to theming. The new component uses ui-next token-based styling and includes minor UX improvements.

Changes & Results

  • Added ContextMenuViewport to @ohif/ui-next — same props API as the legacy component, now with theme-aware styling and UX polish
  • Updated contextMenuUICustomization.ts to import from @ohif/ui-next
  • No changes to the controller, items builder, or any mode/extension customizations

Testing

  1. Load a study and create a measurement annotation
  2. Right-click the annotation — context menu should appear with themed colors
  3. Verify "Delete measurement" and "Add Label" work as expected
  4. Dismiss via Escape or clicking outside

Checklist

PR

  • My Pull Request title is descriptive, accurate and follows the
    semantic-release format and guidelines.

Suggested PR title: feat(ui-next): add ContextMenuViewport component with theme support

Code

  • My code has been well-documented (function documentation, inline comments,
    etc.)

Public Documentation Updates

  • The documentation page has been updated as necessary for any public API
    additions or removals.

Tested Environment

  • OS: macOS Sequoia 15.7.4 (24G517)
  • Node version: 20.19.1
  • Browser: Chrome 147.0.7727.138 (Official Build) (arm64)

Greptile Summary

Migrates the viewport context menu from @ohif/ui (ContextMenu) to @ohif/ui-next (ContextMenuViewport), replacing hardcoded color classes with Tailwind token-based theming and adding a subtle open animation. The props contract and item-action wiring are preserved, so existing controller and menu-item builder code works without changes.

  • ContextMenuViewport is functionally equivalent to the old component; item.action(item, props) still has access to onClose, onShowSubMenu, and onDefault via the ...props spread, so menu close-on-click and submenu navigation continue to work.
  • cursor-default select-none replaces cursor-pointer on interactive items — this matches shadcn/ui conventions but removes the pointer-cursor affordance users typically expect from clickable menu rows.
  • defaultPosition is declared in the TypeScript interface but is never read by the component (positioning is managed externally by the dialog service).

Confidence Score: 4/5

Safe to merge; the migration is behaviorally equivalent and all observations are minor style and polish items.

The component faithfully mirrors the old ContextMenu's prop wiring and close-on-click behavior. The only tangible change worth a second look is the switch from cursor-pointer to cursor-default on menu items, which removes a standard interactive affordance without a clear design reason.

ContextMenuViewport.tsx — the cursor class and unused interface prop are both worth a quick look before merge.

Important Files Changed

Filename Overview
platform/ui-next/src/components/ContextMenuViewport/ContextMenuViewport.tsx New component replacing the legacy ContextMenu; functionally equivalent with token-based theming, but uses cursor-default instead of cursor-pointer on interactive items and declares an unused defaultPosition prop in the interface.
extensions/default/src/customizations/contextMenuUICustomization.ts Single-line import swap from @ohif/ui ContextMenu to @ohif/ui-next ContextMenuViewport; straightforward and correct.
platform/ui-next/src/components/ContextMenuViewport/index.ts Standard barrel export for the new component; no issues.
platform/ui-next/src/components/index.ts Adds ContextMenuViewport import and export to the ui-next barrel; correctly placed alongside peer components.
Prompt To Fix All With AI
Fix the following 3 code review issues. Work through them one at a time, proposing concise fixes.

---

### Issue 1 of 3
platform/ui-next/src/components/ContextMenuViewport/ContextMenuViewport.tsx:31
**Cursor no longer signals clickability on menu items**

The old component used `cursor-pointer`, but the new one uses `cursor-default`. Context menu items are interactive (they fire `item.action` on click), so users expect the hand cursor as a hover affordance. With `cursor-default`, hovering over any item looks the same as hovering over non-interactive content, which is a subtle but real UX regression — especially for power users who rely on the pointer change before clicking.

### Issue 2 of 3
platform/ui-next/src/components/ContextMenuViewport/ContextMenuViewport.tsx:11-12
`defaultPosition` is declared in the interface but never read in the component body. Since the dialog service handles positioning externally via `uiDialogService.show({ defaultPosition })`, this prop will never be forwarded to the component as a prop, making the declaration dead code.

```suggestion
  [key: string]: unknown;
```

### Issue 3 of 3
platform/ui-next/src/components/ContextMenuViewport/ContextMenuViewport.tsx:16-18
**Empty array bypasses the early-return guard**

The guard `if (!items)` returns `null` only when `items` is `undefined` or `null`. If `ContextMenuItemsBuilder.getMenuItems` returns `[]`, the controller's own `if (!items) return` also passes through, and the component renders an empty container div — a small invisible-but-present box at the calculated position. Adding `|| items.length === 0` to the guard would prevent this; note the old component has the same behavior so this is not a regression introduced by this PR.

Reviews (1): Last reviewed commit: "Refine styling" | Re-trigger Greptile

Greptile also left 3 inline comments on this PR.

Summary by CodeRabbit

  • New Features
    • Updated the context menu experience to use the newer viewport-based menu component.
    • Added support for rendering menu items with optional right-side icons and per-item click actions.
    • The menu only appears when items are provided, and it suppresses the browser’s default context menu behavior.

@netlify

netlify Bot commented May 8, 2026

Copy link
Copy Markdown

Deploy Preview for ohif-dev ready!

Name Link
🔨 Latest commit 7a4b4a3
🔍 Latest deploy log https://app.netlify.com/projects/ohif-dev/deploys/6a3c00ff11dc4b00085ec67d
😎 Deploy Preview https://deploy-preview-6008--ohif-dev.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.
🤖 Make changes Run an agent on this branch

To edit notification comments on pull requests, go to your Netlify project configuration.

Comment thread platform/ui-next/src/components/ContextMenuViewport/ContextMenuViewport.tsx Outdated
@jbocce

jbocce commented May 11, 2026

Copy link
Copy Markdown
Collaborator

@claude review

@jbocce jbocce left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Thanks for this. One small comment.

@coderabbitai

coderabbitai Bot commented Jun 24, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: d94d8023-8e39-4f47-bfea-c4a15efaaddd

📥 Commits

Reviewing files that changed from the base of the PR and between 1340448 and 16ef30a.

📒 Files selected for processing (1)
  • platform/ui-next/src/components/ContextMenuViewport/ContextMenuViewport.tsx
💤 Files with no reviewable changes (1)
  • platform/ui-next/src/components/ContextMenuViewport/ContextMenuViewport.tsx

📝 Walkthrough

Walkthrough

Adds a new ContextMenuViewport React component to @ohif/ui-next that renders a context menu from an items array with optional icons and click handlers. The component is barrel-exported from @ohif/ui-next, and the default context menu UI customization is updated to use it instead of the legacy ContextMenu from @ohif/ui.

Changes

ContextMenuViewport Component and Customization

Layer / File(s) Summary
ContextMenuViewport component and barrel exports
platform/ui-next/src/components/ContextMenuViewport/ContextMenuViewport.tsx, platform/ui-next/src/components/ContextMenuViewport/index.ts, platform/ui-next/src/components/index.ts
Defines ContextMenuViewportProps with an optional items array (label, action, optional iconRight). Implements ContextMenuViewport which returns null when items is absent; otherwise renders a data-cy="context-menu" container mapping each item to a data-cy="context-menu-item" row, calls item.action(item, props) on click, conditionally renders Icons.ByName for iconRight, and prevents the browser default context menu. Barrel-exported via the component's own index and added to the @ohif/ui-next components index.
Default context menu UI customization swap
extensions/default/src/customizations/contextMenuUICustomization.ts
Replaces the ContextMenu import from @ohif/ui with ContextMenuViewport from @ohif/ui-next and assigns ContextMenuViewport as the ui.contextMenu customization value.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~8 minutes

Poem

🐰 A menu hops into the viewport with flair,
New ContextMenuViewport floating in air.
Icons on the right, each action in a row,
From ui-next it blooms with a soft gentle glow.
The old ContextMenu naps in @ohif/ui,
While the new one takes center stage — hooray, it's true! 🥕

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title is concise, accurate, and describes the main migration to ui-next with theming support.
Description check ✅ Passed All required template sections are present and filled in with context, changes, testing, and checklist details.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 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 `@platform/ui-next/src/components/ContextMenuViewport/ContextMenuViewport.tsx`:
- Around line 27-32: The context menu actions in ContextMenuViewport are
rendered as non-interactive divs, so they cannot be reached or activated with
the keyboard. Update the item rendering in ContextMenuViewport to use a semantic
button-like control for each action, preserve the existing item.action(item,
props) click behavior, and make sure the interactive styling uses cursor-pointer
instead of cursor-default. Ensure the unique item row wrapper and its onClick
handler remain tied to the same ContextMenuViewport mapping logic.
- Around line 16-18: The ContextMenuViewport rendering guard only checks for
falsy items, so an empty array still creates a blank menu container. Update the
conditional in ContextMenuViewport to also skip rendering when the items
collection is empty, using the existing items check before the menu shell is
returned. Keep the fix localized to ContextMenuViewport so empty menus do not
render at all.
🪄 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: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 4cdcef4a-0595-4903-b86c-8ec42560a877

📥 Commits

Reviewing files that changed from the base of the PR and between 20632f9 and 1340448.

📒 Files selected for processing (4)
  • extensions/default/src/customizations/contextMenuUICustomization.ts
  • platform/ui-next/src/components/ContextMenuViewport/ContextMenuViewport.tsx
  • platform/ui-next/src/components/ContextMenuViewport/index.ts
  • platform/ui-next/src/components/index.ts

Comment on lines +16 to +18
if (!items) {
return null;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Avoid rendering an empty menu container when items is empty.

Line 16 only checks for falsy values; [] still renders a blank menu shell.

Suggested fix
-  if (!items) {
+  if (!items?.length) {
     return null;
   }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (!items) {
return null;
}
if (!items?.length) {
return null;
}
🤖 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 `@platform/ui-next/src/components/ContextMenuViewport/ContextMenuViewport.tsx`
around lines 16 - 18, The ContextMenuViewport rendering guard only checks for
falsy items, so an empty array still creates a blank menu container. Update the
conditional in ContextMenuViewport to also skip rendering when the items
collection is empty, using the existing items check before the menu shell is
returned. Keep the fix localized to ContextMenuViewport so empty menus do not
render at all.

Comment on lines +27 to +32
<div
key={index}
data-cy="context-menu-item"
onClick={() => item.action(item, props)}
className="hover:bg-accent hover:text-accent-foreground flex cursor-default select-none items-center justify-between rounded-sm px-2 py-1.5 text-base outline-none"
>

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Use keyboard-focusable controls for menu actions.

Line 27 renders clickable rows as <div>, so actions are not keyboard-focusable/activatable. Line 31 also uses cursor-default on interactive elements. Use a semantic <button> (or equivalent keyboard handlers) and cursor-pointer.

Suggested fix
-        <div
+        <button
+          type="button"
           key={index}
           data-cy="context-menu-item"
           onClick={() => item.action(item, props)}
-          className="hover:bg-accent hover:text-accent-foreground flex cursor-default select-none items-center justify-between rounded-sm px-2 py-1.5 text-base outline-none"
+          className="hover:bg-accent hover:text-accent-foreground flex w-full cursor-pointer select-none items-center justify-between rounded-sm px-2 py-1.5 text-base outline-none"
         >
           <span>{item.label}</span>
           {item.iconRight && (
             <Icons.ByName
               name={item.iconRight}
               className="inline"
             />
           )}
-        </div>
+        </button>
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<div
key={index}
data-cy="context-menu-item"
onClick={() => item.action(item, props)}
className="hover:bg-accent hover:text-accent-foreground flex cursor-default select-none items-center justify-between rounded-sm px-2 py-1.5 text-base outline-none"
>
<button
type="button"
key={index}
data-cy="context-menu-item"
onClick={() => item.action(item, props)}
className="hover:bg-accent hover:text-accent-foreground flex w-full cursor-pointer select-none items-center justify-between rounded-sm px-2 py-1.5 text-base outline-none"
>
🤖 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 `@platform/ui-next/src/components/ContextMenuViewport/ContextMenuViewport.tsx`
around lines 27 - 32, The context menu actions in ContextMenuViewport are
rendered as non-interactive divs, so they cannot be reached or activated with
the keyboard. Update the item rendering in ContextMenuViewport to use a semantic
button-like control for each action, preserve the existing item.action(item,
props) click behavior, and make sure the interactive styling uses cursor-pointer
instead of cursor-default. Ensure the unique item row wrapper and its onClick
handler remain tied to the same ContextMenuViewport mapping logic.

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