Skip to content

feat(ui-next): Adds toggle state for ToolButton and Crosshair example#5914

Open
dan-rukas wants to merge 5 commits intoOHIF:masterfrom
dan-rukas:feat/crosshair-and-toolbutton-toggle
Open

feat(ui-next): Adds toggle state for ToolButton and Crosshair example#5914
dan-rukas wants to merge 5 commits intoOHIF:masterfrom
dan-rukas:feat/crosshair-and-toolbutton-toggle

Conversation

@dan-rukas
Copy link
Copy Markdown
Member

@dan-rukas dan-rukas commented Mar 23, 2026

Context

Adds new toggle state for ToolButton component and reference implementation for the Crosshair tool.

Review the deploy preview: https://crosshair-simple-toggle.netlify.app/

ToolButton-toggle-demo

Changes & Results

ToolButton changes in ui-next:

  • Adds a new isToggled visual state to the ToolButton component for tools that persist when switching to another tool
  • Implements crosshair toggle as a reference use case: click to turn on/off without changing the active tool
  • Adds ToolCrosshairChecked icon (crosshair with checkmark badge)

Crosshair Implementation (reference — can be extended)

The crosshair toggle demonstrates the new state with a minimal implementation:

  • Toolbar button toggles crosshairs between Passive (visible, repositionable via handles) and Disabled (off)
  • Active tool is never changed — user's current tool stays active
  • Icon swaps to a checked variant when toggled on
  • disableOnPassive set to false so crosshairs survive tool switches

This implementation is intentionally scoped as a starting point. It can be extended to support additional interactions such as:

  • Modifier key hold to temporarily make crosshairs the active tool for full viewport interaction
  • The evaluator and command can be updated to handle Active mode without any changes to the ToolButton component itself

Changed files:

  • platform/ui-next/src/components/ToolButton/ToolButton.tsx — new isToggled prop
  • platform/ui-next/src/components/Icons/Sources/Tools.tsx — new ToolCrosshairChecked icon
  • platform/ui-next/src/components/Icons/Icons.tsx — icon registration
  • extensions/cornerstone/src/commandsModule.tstoggleCrosshairsToolbar command
  • extensions/cornerstone/src/getToolbarModule.tsxcrosshairToggle evaluator
  • modes/basic/src/toolbarButtons.ts — crosshair button config
  • modes/basic/src/initToolGroups.tsdisableOnPassive: false

Testing

  • Verify existing toolbar buttons render unchanged
  • In MPR mode: click crosshair button → crosshair lines appear, icon changes
    to checked variant with highlight color
  • Switch to another tool → crosshairs stay visible, button stays toggled
  • Click crosshair button again → crosshairs disappear, button returns to default
  • Verify active tool is never interrupted during toggle
  • Click directly on crosshair lines in viewport → can reposition (Passive mode)
  • Switch to non-MPR viewport → crosshair button shows disabled
  • Verify Reference Lines and other existing toggle tools still work

Checklist

PR

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

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), Apple M4 Pro
  • Node version: 20.19.1
  • Browser: Chrome, macOS (latest) 146.0.7680.80 (Official Build) (arm64)

Greptile Summary

This PR introduces a new isToggled visual state to the ToolButton component and implements the Crosshairs tool as its first reference use case, allowing users to toggle crosshairs on/off in MPR mode without changing the active tool. The implementation is well-structured and follows existing patterns in the codebase.

Key changes:

  • ToolButton gains an isToggled prop with a distinct visual style (text-highlight on transparent background), correctly placed in the class-priority chain (disabled > isActive > isToggled > default)
  • New evaluate.cornerstoneTool.crosshairToggle evaluator returns isToggled state and swaps icons dynamically between tool-crosshair and tool-crosshair-checked
  • New toggleCrosshairsToolbar command toggles the tool between Passive and Disabled modes across specified tool groups
  • disableOnPassive: false correctly prevents crosshairs from being swept away when another tool is activated
  • New ToolCrosshairChecked SVG icon provides clear visual feedback for the toggled-on state
  • The isOn guard in both the evaluator and the command omits Enums.ToolModes.Active — if Crosshairs is ever set Active via a keyboard shortcut or another command path, the button will appear un-toggled and clicking will call setToolPassive instead of setToolDisabled, leaving crosshairs visible without the user intending it
  • The same isOn logic is copy-pasted verbatim into both files, so the two must be kept in sync manually
  • aria-pressed is absent from the inner <Button> in ToolButton, meaning screen readers cannot announce the toggle state to assistive-technology users

Confidence Score: 3/5

  • Safe to merge for the happy path, but the Active-mode omission in both the command and evaluator can silently leave crosshairs visible when the user expects them to be toggled off.
  • The UI component changes and icon additions are clean with no issues. The logic concern—excluding Active from the isOn check in both toggleCrosshairsToolbar and the evaluator—is small but can produce incorrect toggle behavior in a realistic scenario (keyboard shortcut activating Crosshairs), and fixing it in one place without the other would still leave the bug. The duplication makes it easy to miss. These two fixes should be applied before merging.
  • extensions/cornerstone/src/commandsModule.ts and extensions/cornerstone/src/getToolbarModule.tsx — both need Active added to the isOn check and ideally share the logic.

Important Files Changed

Filename Overview
platform/ui-next/src/components/ToolButton/ToolButton.tsx Adds isToggled prop with a new toggledClasses visual state; the class precedence chain is correct, but aria-pressed is missing from the inner <Button> for accessibility.
extensions/cornerstone/src/getToolbarModule.tsx New evaluate.cornerstoneTool.crosshairToggle evaluator correctly disables the button for non-MPR viewports, but excludes Active mode from isOn, and duplicates toggle-state logic with commandsModule.ts.
extensions/cornerstone/src/commandsModule.ts New toggleCrosshairsToolbar action toggles the tool between Passive and Disabled across specified tool groups; same Active-mode omission as the evaluator means the command misfires when crosshairs is Active.
modes/basic/src/initToolGroups.ts Changes disableOnPassive to false so crosshairs survive tool switches — correct and intentional for the toggle-based UX.
modes/basic/src/toolbarButtons.ts Crosshair button updated to use the new command/evaluator pair and adds a descriptive tooltip; no issues.
platform/ui-next/src/components/Icons/Sources/Tools.tsx New ToolCrosshairChecked SVG icon added with a unique clipPath id; no collision with the existing ToolCrosshair which uses no clip path.
platform/ui-next/src/components/Icons/Icons.tsx Standard icon registration for ToolCrosshairChecked and the tool-crosshair-checked string key; no issues.

Sequence Diagram

sequenceDiagram
    participant User
    participant ToolButton
    participant ToolbarService
    participant toggleCrosshairsToolbar
    participant ToolGroup
    participant crosshairToggleEvaluator

    User->>ToolButton: Click crosshair button
    ToolButton->>ToolbarService: onInteraction({ itemId: 'Crosshairs' })
    ToolbarService->>toggleCrosshairsToolbar: execute({ itemId, toolGroupIds: ['mpr'] })
    toggleCrosshairsToolbar->>ToolGroup: getToolGroup('mpr')
    toggleCrosshairsToolbar->>ToolGroup: getToolOptions('Crosshairs').mode
    alt isOn (Passive or Enabled)
        toggleCrosshairsToolbar->>ToolGroup: setToolDisabled('Crosshairs')
    else isOff (Disabled)
        toggleCrosshairsToolbar->>ToolGroup: setToolPassive('Crosshairs')
    end

    Note over ToolGroup: State change triggers re-evaluation

    crosshairToggleEvaluator->>ToolGroup: getToolGroupForViewport(viewportId)
    crosshairToggleEvaluator->>ToolGroup: getToolOptions('Crosshairs').mode
    crosshairToggleEvaluator-->>ToolButton: { isToggled, icon }
    ToolButton-->>User: Visual state update (icon swap + highlight color)
Loading

Comments Outside Diff (1)

  1. platform/ui-next/src/components/ToolButton/ToolButton.tsx, line 91-103 (link)

    P2 Missing aria-pressed attribute for accessibility

    The data-toggled attribute is added to the wrapping <span> for DOM querying, but there is no aria-pressed on the <Button> itself. Without aria-pressed, screen readers cannot announce the toggle state change to assistive-technology users.

Reviews (1): Last reviewed commit: "Added correct disabled text" | Re-trigger Greptile

Greptile also left 3 inline comments on this PR.

(5/5) You can turn off certain types of comments like style here!

@netlify
Copy link
Copy Markdown

netlify bot commented Mar 23, 2026

Deploy Preview for ohif-dev canceled.

Name Link
🔨 Latest commit 9dd75d4
🔍 Latest deploy log https://app.netlify.com/projects/ohif-dev/deploys/69c29251063f36000856c1a5

Comment on lines +446 to +447
const isOn = currentMode === Enums.ToolModes.Passive ||
currentMode === Enums.ToolModes.Enabled;
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.

P1 Active mode excluded from isOn check

The isOn detection uses Passive || Enabled but omits Active. If Crosshairs is activated through an alternative path (e.g., a keyboard binding or another command that calls setToolActive), the button will appear un-toggled even though the tool is visually active in the viewports. The corresponding command logic (line 1054–1055 in commandsModule.ts) shares the same omission, so clicking the button when Crosshairs is already Active would try to enable Passive mode again instead of disabling it, leaving the crosshairs on.

For consistency with toggleActiveDisabledToolbar, consider including Active:

Suggested change
const isOn = currentMode === Enums.ToolModes.Passive ||
currentMode === Enums.ToolModes.Enabled;
const isOn = currentMode === Enums.ToolModes.Passive ||
currentMode === Enums.ToolModes.Enabled ||
currentMode === Enums.ToolModes.Active;

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

The omission of Active is intentional.

See the PR message above - this implementation scopes the toolbar button to a pure on/off toggle (Passive ↔ Disabled). The crosshair tool is never set to Active through the toolbar. Active mode will be handled separately at the viewport level in @sedghi's upcoming work via a modifier key interaction. The toolbar button intentionally stays in its toggled appearance during that interaction. Any changes to support Active mode would be part of that follow-up work.

Comment on lines +1054 to +1055
const isOn = currentMode === Enums.ToolModes.Passive ||
currentMode === Enums.ToolModes.Enabled;
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.

P1 Active mode excluded from isOn check (same as evaluator)

This mirrors the omission in the evaluator: if Crosshairs is somehow in Active mode (keyboard shortcut or another command), isOn will be false, and clicking the toggle button will call setToolPassive instead of setToolDisabled, leaving crosshairs active without feedback.

The same fix applies here:

Suggested change
const isOn = currentMode === Enums.ToolModes.Passive ||
currentMode === Enums.ToolModes.Enabled;
const isOn = currentMode === Enums.ToolModes.Passive ||
currentMode === Enums.ToolModes.Enabled ||
currentMode === Enums.ToolModes.Active;

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Same reasoning as above. Active mode is out of scope for this PR and will be handled in follow-up work.

Comment on lines +433 to +455
name: 'evaluate.cornerstoneTool.crosshairToggle',
evaluate: ({ viewportId, button, disabledText }) => {
const toolGroup = toolGroupService.getToolGroupForViewport(viewportId);
if (!toolGroup) {
return;
}

const toolName = toolbarService.getToolNameForButton(button);
if (!toolGroup.hasTool(toolName)) {
return getDisabledState(disabledText);
}

const currentMode = toolGroup.getToolOptions(toolName).mode;
const isOn = currentMode === Enums.ToolModes.Passive ||
currentMode === Enums.ToolModes.Enabled;

return {
disabled: false,
isActive: false,
isToggled: isOn,
icon: isOn ? 'tool-crosshair-checked' : 'tool-crosshair',
};
},
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.

P2 Duplicated isOn detection logic

The isOn evaluation (Passive || Enabled) is copy-pasted identically into both evaluate.cornerstoneTool.crosshairToggle (here) and toggleCrosshairsToolbar in commandsModule.ts. When one changes (e.g., adding Active to the check), the other must be updated manually.

Consider extracting this into a shared helper, or at minimum leaving a comment cross-referencing the two sites so they stay in sync.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

The two checks should stay in sync. I added cross-reference comments in both locations explaining that they must agree on which modes count as "on," and noting that both need updating if Active mode support is added.

@dan-rukas
Copy link
Copy Markdown
Member Author

@sedghi Greptile's comments above focus on some of the Active states of Crosshairs I'm intentionally leaving out of the scope of this PR. I added a few cross-reference comments in the code.

Again, the Crosshairs implementation is for reference only, so feel free to modify, extend, etc.

sedghi added 2 commits March 24, 2026 07:39
…nsistency in command naming and update related references in the toolbar buttons.
…users to set modifier keys for jump actions. Update User Preferences modal to display and manage mouse shortcuts, enhancing user experience with tool interactions.
@cypress
Copy link
Copy Markdown

cypress bot commented Mar 24, 2026

Viewers    Run #6119

Run Properties:  status check passed Passed #6119  •  git commit 9dd75d4483: Implement mouse shortcut customization for crosshairs tool, allowing users to se...
Project Viewers
Branch Review feat/crosshair-and-toolbutton-toggle
Run status status check passed Passed #6119
Run duration 02m 23s
Commit git commit 9dd75d4483: Implement mouse shortcut customization for crosshairs tool, allowing users to se...
Committer Alireza
View all properties for this run ↗︎

Test results
Tests that failed  Failures 0
Tests that were flaky  Flaky 0
Tests that did not run due to a developer annotating a test with .skip  Pending 0
Tests that did not run due to a failure in a mocha hook  Skipped 0
Tests that passed  Passing 37
View all changes introduced in this branch ↗︎

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