Skip to content

Conversation

@s4l4x
Copy link

@s4l4x s4l4x commented Nov 3, 2025

Summary

Adds an interactive 3D joystick control for Vector3d inputs with visual feedback and keyboard modifier support for plane switching.

leva 3d joystick 2x

Key Features

  • 3D Visual Indicator (JoyCube): New JoyCube component renders a 3D cube that rotates to indicate the current editing plane
  • Modifier Key Support: Switch between XY, XZ, and ZY planes using:
    • No modifier: XY plane (default)
    • Ctrl/^: XZ plane
    • Cmd/Win: ZY plane
  • Cross-platform Keys: Automatically detects OS and shows appropriate modifier key labels (⌘ on Mac, win on Windows/Linux)
  • Visual Plane Selector: Three buttons below the joystick show the current active plane
  • Smooth Transitions: Cube rotations animate smoothly between plane switches

Technical Changes

New Files

  • packages/leva/src/components/UI/JoyCube.tsx - 3D cube component with configurable face layers
  • packages/leva/src/components/UI/Joystick3d.tsx - 3D joystick wrapper with plane switching logic
  • packages/leva/src/components/UI/StyledJoystick3d.ts - Stitches-based styled components for 3D joystick UI
  • packages/leva/src/hooks/useKeyPress.ts - Hook for detecting modifier key presses

Modified Files

  • packages/leva/src/components/UI/Joystick.tsx - Added children prop support for custom visual indicators
  • packages/leva/src/components/UI/Misc.tsx - Enhanced Portal component with container prop
  • packages/leva/src/plugins/Vector3d/Vector3d.tsx - Integrated Joystick3d component

Code Quality

  • Follows all Leva codebase conventions and patterns
  • Uses TypeScript with proper type definitions
  • Styled with Stitches matching existing theme tokens
  • Clean component structure with separated concerns

Supersedes

This PR supersedes #433 with a significantly enhanced implementation featuring better UX, visual feedback, and cross-platform support.

Summary by CodeRabbit

  • New Features

    • Interactive 2D and 3D joystick controls with plane selector and 3D cube visualization.
    • Optional joystick support for Vector3 inputs (including an invert‑Y option).
    • Button labels now accept JSX elements or strings.
    • New hook to detect key press state for modifier-based controls.
  • UI Enhancements

    • New 3D joystick visuals, playground, controls, and related UI exports.
  • Docs / Examples

    • Added Vector3 stories demonstrating joystick usage and inverted Y option.

@changeset-bot
Copy link

changeset-bot bot commented Nov 3, 2025

🦋 Changeset detected

Latest commit: 5ebdd53

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
leva Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@vercel
Copy link

vercel bot commented Nov 3, 2025

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

Project Deployment Preview Comments Updated (UTC)
leva Ready Ready Preview Comment Nov 10, 2025 9:51am

@s4l4x
Copy link
Author

s4l4x commented Nov 3, 2025

@gsimone here's an updated version for 1.0. Check it out in the storybook:

image

@codesandbox-ci
Copy link

codesandbox-ci bot commented Nov 3, 2025

This pull request is automatically built and testable in CodeSandbox.

To see build info of the built libraries, click here or the icon next to each commit SHA.

Latest deployment of this branch, based on commit 5ebdd53:

Sandbox Source
leva-minimal Configuration
leva-busy Configuration
leva-scroll Configuration
leva-advanced-panels Configuration
leva-ui Configuration
leva-theme Configuration
leva-transient Configuration
leva-plugin-plot Configuration
leva-plugin-bezier Configuration
leva-plugin-spring Configuration
leva-plugin-dates Configuration
leva-custom-plugin Configuration

@gsimone gsimone changed the title Feat/joystick3d with modifier keys feat: adds joystick3d with modifier keys Nov 8, 2025
@gsimone gsimone requested a review from Copilot November 8, 2025 10:58
Copy link
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 adds 3D joystick support to Vector3d inputs, allowing users to manipulate 3D vectors through an interactive joystick interface with plane switching capabilities. The implementation reuses the existing 2D joystick component and extends it to support three planes (XY, XZ, ZY) that can be switched using keyboard modifiers.

Key changes:

  • Extracted and refactored the Joystick component from Vector2d to be reusable across both Vector2d and Vector3d
  • Added a new Joystick3d component that wraps the base Joystick with plane-switching controls
  • Introduced a useKeyPress hook for tracking keyboard modifier states
  • Added visual feedback via a 3D cube (JoyCube) showing the current active plane

Reviewed Changes

Copilot reviewed 14 out of 14 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
packages/leva/stories/inputs/Vector.stories.tsx Added Storybook stories demonstrating Vector3d joystick functionality with various configurations
packages/leva/src/plugins/Vector3d/vector3d-types.ts Extended type definition to include joystick configuration option
packages/leva/src/plugins/Vector3d/index.ts Added normalize function to handle joystick settings during plugin initialization
packages/leva/src/plugins/Vector3d/Vector3d.tsx Integrated Joystick3d component into Vector3d rendering logic
packages/leva/src/plugins/Vector2d/Vector2d.tsx Updated to import Joystick from shared UI components location
packages/leva/src/hooks/useKeyPress.ts New hook for detecting and tracking keyboard key press states
packages/leva/src/hooks/index.ts Exported the new useKeyPress hook
packages/leva/src/components/UI/index.ts Exported new UI components (Joystick, JoyCube, Joystick3d)
packages/leva/src/components/UI/StyledJoystick3d.ts Styled components for 3D joystick UI elements including cube faces and button controls
packages/leva/src/components/UI/StyledJoystick.ts Extracted shared styled components from Vector2d for reuse
packages/leva/src/components/UI/Joystick3d.tsx New component managing 3D joystick with plane selection and keyboard controls
packages/leva/src/components/UI/Joystick.tsx Refactored joystick component to support both 2D and 3D vectors
packages/leva/src/components/UI/JoyCube.tsx 3D cube visualization component for showing active plane
packages/leva/src/components/Button/Button.tsx Extended label prop type to accept JSX.Element for custom button content

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@coderabbitai
Copy link

coderabbitai bot commented Nov 8, 2025

Note

Other AI code review bot(s) detected

CodeRabbit has detected other AI code review bot(s) in this pull request and will avoid duplicating their findings in the review comments. This may lead to a less comprehensive review.

Walkthrough

Adds 2D and 3D joystick UI (Joystick, Joystick3d, JoyCube) with styling and a useKeyPress hook, integrates joystick support into Vector3d (normalize wrapper and new joystick setting), expands Button.label to accept JSX.Element, updates imports/exports, and adds stories demonstrating joystick usage.

Changes

Cohort / File(s) Summary
Button API Expansion
packages/leva/src/components/Button/Button.tsx
ButtonProps updated: label now accepts string | JSX.Element.
2D Joystick UI & Styles
packages/leva/src/components/UI/Joystick.tsx, packages/leva/src/components/UI/StyledJoystick.ts
Added Joystick component (draggable 2D joystick with bounds, step multiplier, out-of-bounds periodic updates) and styled exports: JoystickTrigger, JoystickPlayground, JoystickGrid.
3D Joystick, Cube & Styles
packages/leva/src/components/UI/Joystick3d.tsx, packages/leva/src/components/UI/JoyCube.tsx, packages/leva/src/components/UI/StyledJoystick3d.ts
Added Joystick3d (plane switching via modifier keys, maps 3D→2D), JoyCube visual component, and 3D styling components (StyledJoyCube, StyledJoyCubeFace, JoystickButtons, ButtonLabelContainer, PlaneLabel, KeyLabel).
UI Barrel Exports
packages/leva/src/components/UI/index.ts
Re-exported Joystick, JoyCube, and Joystick3d.
Keyboard Hook
packages/leva/src/hooks/useKeyPress.ts, packages/leva/src/hooks/index.ts
New useKeyPress(targetKey: string): boolean hook and exported it from hooks index.
Vector2d / Vector3d Integration
packages/leva/src/plugins/Vector2d/Vector2d.tsx, packages/leva/src/plugins/Vector3d/Vector3d.tsx
Vector2d now imports Joystick from UI barrel; Vector3d wraps rendering in a Container and conditionally renders Joystick3d when settings.joystick is set.
Vector3d Plugin Normalize & Types
packages/leva/src/plugins/Vector3d/index.ts, packages/leva/src/plugins/Vector3d/vector3d-types.ts
Introduced local plugin alias and normalize wrapper to inject default joystick setting; extended InternalVector3dSettings with joystick: boolean | 'invertY'.
Stories
packages/leva/stories/inputs/Vector.stories.tsx
Added three Vector3 stories demonstrating joystick usage and invertY mode.
Changeset
.changeset/pink-cups-refuse.md
New changeset documenting joystick3d feature and version bump.

Sequence Diagram

sequenceDiagram
    participant User
    participant Joystick3d
    participant useKeyPress
    participant JoyCube
    participant Joystick
    participant Vector3dPlugin

    User->>Joystick3d: drag + modifier keys
    Joystick3d->>useKeyPress: subscribe to modifier keys
    useKeyPress-->>Joystick3d: key pressed state
    Joystick3d->>JoyCube: update orientation visuals
    Joystick3d->>Joystick: provide mapped 2D settings for active plane
    Joystick->>Joystick: handle drag, clamp, step multiplier, out-of-bounds loop
    Joystick->>Vector3dPlugin: onUpdate(new vector)
    Vector3dPlugin-->>User: re-rendered vector value
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Areas to focus on:

  • packages/leva/src/components/UI/Joystick.tsx — drag handling, bounds/clamping, out-of-bounds lifecycle, refs and cleanup.
  • packages/leva/src/components/UI/Joystick3d.tsx — keyboard plane switching, mapping 3D→2D settings, platform modifier labeling, and useKeyPress integration.
  • packages/leva/src/components/UI/StyledJoystick3d.ts — complex 3D CSS transforms and face selectors.
  • packages/leva/src/plugins/Vector3d/index.ts & vector3d-types.ts — normalize wrapper behavior and type compatibility for new joystick setting.
  • Story updates for Vector3 joystick and invertY behavior.

Poem

🐰
I nudged the cube with whiskered cheer,
Joysticks hummed and planes appear,
Keys and drags make vectors sing,
A hop, a turn — the values spring,
Hooray — the UI bounces near!

Pre-merge checks and finishing touches

❌ 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%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat: adds joystick3d with modifier keys' accurately and specifically describes the main feature addition—a 3D joystick component with keyboard modifier support for plane switching.
✨ Finishing touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

📜 Recent review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 99db1c5 and 5ebdd53.

📒 Files selected for processing (1)
  • .changeset/pink-cups-refuse.md (1 hunks)
🔇 Additional comments (1)
.changeset/pink-cups-refuse.md (1)

1-5: Verify the version bump type for this feature addition.

The changeset marks this as a patch bump, but PR #582 adds new public components (Joystick3d, JoyCube), hooks (useKeyPress), and styled components. Feature additions with public API exports typically warrant a minor version bump (backward-compatible new features) rather than a patch bump, which is reserved for bug fixes.

Please confirm that patch is the correct bump type according to the project's versioning strategy.


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 and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

♻️ Duplicate comments (2)
packages/leva/src/components/UI/StyledJoystick.ts (1)

46-53: Fix the zIndex typo.

zindex isn’t a valid CSS-in-JS key, so the handle/grid layering never gets the intended stacking order. Please change both occurrences to zIndex.

-    zindex: 100,
+    zIndex: 100,-    zindex: 10,
+    zIndex: 10,

Also applies to: 66-70

packages/leva/src/components/UI/Joystick3d.tsx (1)

54-56: Use a no-op callback instead of returning a string.

onClick={() => ''} is a leftover stub; keep it as a proper no-op to avoid pointless string returns.

-              onClick={() => ''}
+              onClick={() => {}}
🧹 Nitpick comments (1)
packages/leva/src/plugins/Vector3d/Vector3d.tsx (1)

6-6: Consider extracting Container to a shared location.

Importing Container from Vector2d creates an inter-plugin dependency, which is unconventional. While this works pragmatically, consider extracting Container to a shared UI module (e.g., components/UI/Container.tsx) for better separation of concerns.

For now, the implementation mirrors Vector2d correctly and functions as intended.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 53f05b0 and cefdae7.

📒 Files selected for processing (14)
  • packages/leva/src/components/Button/Button.tsx (1 hunks)
  • packages/leva/src/components/UI/JoyCube.tsx (1 hunks)
  • packages/leva/src/components/UI/Joystick.tsx (1 hunks)
  • packages/leva/src/components/UI/Joystick3d.tsx (1 hunks)
  • packages/leva/src/components/UI/StyledJoystick.ts (1 hunks)
  • packages/leva/src/components/UI/StyledJoystick3d.ts (1 hunks)
  • packages/leva/src/components/UI/index.ts (1 hunks)
  • packages/leva/src/hooks/index.ts (1 hunks)
  • packages/leva/src/hooks/useKeyPress.ts (1 hunks)
  • packages/leva/src/plugins/Vector2d/Vector2d.tsx (1 hunks)
  • packages/leva/src/plugins/Vector3d/Vector3d.tsx (1 hunks)
  • packages/leva/src/plugins/Vector3d/index.ts (1 hunks)
  • packages/leva/src/plugins/Vector3d/vector3d-types.ts (1 hunks)
  • packages/leva/stories/inputs/Vector.stories.tsx (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (11)
packages/leva/src/components/Button/Button.tsx (1)
packages/leva/src/components/UI/Label.tsx (4)
  • Label (53-94)
  • RawLabel (24-51)
  • OptionalToggle (7-20)
  • disable (15-15)
packages/leva/src/components/UI/StyledJoystick.ts (1)
packages/leva/src/styles/stitches.config.ts (3)
  • touchAction (133-137)
  • display (118-122)
  • display (114-117)
packages/leva/src/plugins/Vector3d/vector3d-types.ts (1)
packages/leva/src/plugins/Vector/vector-types.ts (1)
  • InternalVectorSettings (55-57)
packages/leva/src/components/UI/Joystick.tsx (4)
packages/leva/src/plugins/Vector2d/vector2d-types.ts (1)
  • Vector2dProps (7-7)
packages/leva/src/utils/event.ts (1)
  • multiplyStep (1-1)
packages/leva/src/components/UI/StyledJoystick.ts (3)
  • JoystickTrigger (3-24)
  • JoystickPlayground (26-54)
  • JoystickGrid (56-81)
packages/leva/src/components/UI/Misc.tsx (1)
  • Portal (7-14)
packages/leva/src/plugins/Vector3d/index.ts (5)
packages/leva/src/plugins/Vector/vector-plugin.ts (1)
  • getVectorPlugin (166-175)
packages/leva/src/plugins/Vector3d/vector3d-types.ts (1)
  • InternalVector3dSettings (4-6)
packages/leva/src/plugin.ts (3)
  • createInternalPlugin (49-53)
  • normalize (75-88)
  • format (103-107)
packages/leva/src/plugins/Vector3d/Vector3d.tsx (1)
  • Vector3dComponent (8-19)
packages/leva/src/types/internal.ts (1)
  • InternalPlugin (95-98)
packages/leva/src/components/UI/Joystick3d.tsx (7)
packages/leva/src/plugins/Vector3d/vector3d-types.ts (1)
  • Vector3dProps (7-7)
packages/leva/src/hooks/useKeyPress.ts (1)
  • useKeyPress (9-36)
packages/leva/src/plugins/Vector2d/vector2d-types.ts (1)
  • InternalVector2dSettings (4-6)
packages/leva/src/components/UI/Joystick.tsx (1)
  • Joystick (15-141)
packages/leva/src/components/UI/JoyCube.tsx (1)
  • JoyCube (12-53)
packages/leva/src/components/UI/StyledJoystick3d.ts (4)
  • JoystickButtons (4-18)
  • ButtonLabelContainer (20-27)
  • KeyLabel (36-44)
  • PlaneLabel (29-34)
packages/leva/src/components/Button/Button.tsx (1)
  • Button (11-20)
packages/leva/src/components/UI/StyledJoystick3d.ts (1)
packages/leva/src/components/UI/StyledJoystick.ts (1)
  • JoystickGrid (56-81)
packages/leva/src/components/UI/JoyCube.tsx (1)
packages/leva/src/components/UI/StyledJoystick3d.ts (2)
  • StyledJoyCube (52-157)
  • StyledJoyCubeFace (46-50)
packages/leva/src/hooks/useKeyPress.ts (1)
packages/leva/src/components/Leva/Filter.tsx (4)
  • handleShortcut (99-107)
  • ref (10-48)
  • event (100-104)
  • window (106-106)
packages/leva/src/plugins/Vector2d/Vector2d.tsx (1)
packages/plugin-spring/src/Spring.tsx (1)
  • Spring (8-22)
packages/leva/src/plugins/Vector3d/Vector3d.tsx (3)
packages/leva/src/plugins/Vector2d/Vector2d.tsx (1)
  • Container (8-17)
packages/leva/src/plugins/Vector/Vector.tsx (2)
  • Container (45-58)
  • Vector (84-111)
packages/leva/src/components/UI/Joystick3d.tsx (1)
  • Joystick3d (24-62)
🔇 Additional comments (8)
packages/leva/src/components/Button/Button.tsx (1)

8-8: LGTM! Clean API extension.

Broadening the label type to accept JSX.Element enables richer button content (e.g., icons, styled text) while maintaining backward compatibility. The implementation correctly handles both types.

packages/leva/src/hooks/index.ts (1)

23-23: LGTM!

The export is properly placed under the UI/Animation Utilities section and follows the existing pattern.

packages/leva/src/components/UI/index.ts (1)

5-7: LGTM!

The new UI component exports follow the established barrel export pattern and properly expose the 3D joystick components to consumers.

packages/leva/src/plugins/Vector2d/Vector2d.tsx (1)

4-4: LGTM! Clean refactor.

Consolidating the import to use the UI barrel improves code organization and aligns with the centralized UI component pattern.

packages/leva/src/plugins/Vector3d/Vector3d.tsx (1)

13-16: LGTM! Consistent integration pattern.

The conditional rendering of Joystick3d and Container wrapper correctly mirrors the Vector2d implementation, providing a consistent API across both plugins.

packages/leva/src/plugins/Vector3d/vector3d-types.ts (1)

4-6: LGTM! Clear type definition.

The joystick property type (boolean | 'invertY') is concise and expressive, allowing both simple enablement and axis inversion configuration.

packages/leva/src/components/UI/JoyCube.tsx (1)

12-53: LGTM! Explicit structure aids clarity.

The component correctly renders up to three depth layers of cube faces, with each layer containing all six faces. While verbose, the explicit structure makes the CSS targeting straightforward and the intent clear.

packages/leva/src/components/UI/StyledJoystick3d.ts (1)

52-157: Well-structured 3D CSS implementation.

The StyledJoyCube component correctly implements a 3D cube visualization using CSS transforms and custom properties. The approach is solid:

  • CSS custom properties provide maintainable size calculations
  • preserve-3d and backfaceVisibility are properly used
  • Face selectors with depth layers (primary/mid/rear) create effective 3D perspective
  • Smooth transitions between orientations

The 18 face selectors are somewhat repetitive, but this explicit approach is clear and appropriate for CSS-in-JS declarations.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (1)
packages/leva/src/components/UI/Joystick.tsx (1)

53-74: Consider caching axis indices to avoid repeated lookups.

The AXIS_KEYS.indexOf(v1) and AXIS_KEYS.indexOf(v2) calls (lines 65-66) execute on every 16ms interval tick. Since v1 and v2 are stable axis keys from settings, you could compute these indices once and reuse them.

For example, compute the indices outside the interval:

  const startOutOfBounds = useCallback(() => {
    if (timeout.current) return
    setIsOutOfBounds(true)
    if (outOfBoundsX.current) set({ x: outOfBoundsX.current * w })
    if (outOfBoundsY.current) set({ y: outOfBoundsY.current * -h })
+   const v1Index = AXIS_KEYS.indexOf(v1)
+   const v2Index = AXIS_KEYS.indexOf(v2)
    timeout.current = window.setInterval(() => {
      onUpdate((v: Vector2d | Vector3d) => {
        const incX = stepV1 * outOfBoundsX.current * stepMultiplier.current
        const incY = yFactor * stepV2 * outOfBoundsY.current * stepMultiplier.current

        return Array.isArray(v)
          ? {
-             [v1]: v[AXIS_KEYS.indexOf(v1)] + incX,
-             [v2]: v[AXIS_KEYS.indexOf(v2)] + incY,
+             [v1]: v[v1Index] + incX,
+             [v2]: v[v2Index] + incY,
            }
          : {
              [v1]: v[v1] + incX,
              [v2]: v[v2] + incY,
            }
      })
    }, 16)
  }, [w, h, onUpdate, set, stepV1, stepV2, v1, v2, yFactor])
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between e734c05 and bb02bdf.

📒 Files selected for processing (1)
  • packages/leva/src/components/UI/Joystick.tsx (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
packages/leva/src/components/UI/Joystick.tsx (5)
packages/leva/src/plugins/Vector2d/vector2d-types.ts (1)
  • Vector2dProps (7-7)
packages/leva/src/utils/event.ts (1)
  • multiplyStep (1-1)
packages/leva/src/components/UI/StyledJoystick.ts (3)
  • JoystickTrigger (3-24)
  • JoystickPlayground (26-54)
  • JoystickGrid (56-81)
packages/leva/src/components/UI/Misc.tsx (1)
  • Portal (7-14)
packages/leva/src/plugins/Vector2d/Joystick.tsx (5)
  • Joystick (13-138)
  • onUpdate (54-68)
  • v (55-67)
  • timeout (49-69)
  • joystickShown (27-33)
🔇 Additional comments (5)
packages/leva/src/components/UI/Joystick.tsx (5)

1-15: LGTM on imports and AXIS_KEYS constant.

The imports are well-organized, and extracting AXIS_KEYS as a module-level constant avoids repeated allocations in the interval callback.


31-37: LGTM on positioning logic.

The layout effect correctly positions the playground relative to the trigger. The non-null assertions are safe since the refs are guaranteed to be populated when joystickShown is true.


82-93: LGTM on keyboard event handling.

The global keyboard listeners allow modifier keys (Shift/Alt) to adjust the step multiplier during drag, and the cleanup properly prevents memory leaks.


95-129: LGTM on drag handler logic.

The drag handler correctly manages:

  • Joystick visibility
  • Movement clamping and out-of-bounds detection
  • Incremental value updates with modifier keys
  • Y-axis inversion via yFactor

The @ts-expect-error suppressions (lines 104, 106) are unfortunate but necessary given that value can be either an array or object, and the axis keys are dynamic strings from settings.


131-142: LGTM on render structure.

The composition pattern using the optional children prop allows Joystick3d to inject custom indicators (like JoyCube), while defaulting to JoystickGrid for 2D use.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (4)
packages/leva/src/components/UI/Joystick.tsx (4)

31-37: Consider optional chaining for safer ref access.

While the joystickShown guard likely ensures refs are set, the non-null assertions bypass TypeScript's safety checks. Optional chaining would be more defensive.

  useLayoutEffect(() => {
    if (joystickShown) {
-     const { top, left, width, height } = joystickRef.current!.getBoundingClientRect()
-     playgroundRef.current!.style.left = left + width / 2 + 'px'
-     playgroundRef.current!.style.top = top + height / 2 + 'px'
+     const rect = joystickRef.current?.getBoundingClientRect()
+     const playground = playgroundRef.current
+     if (rect && playground) {
+       playground.style.left = rect.left + rect.width / 2 + 'px'
+       playground.style.top = rect.top + rect.height / 2 + 'px'
+     }
    }
  }, [joystickShown])

53-74: Consider optimizing axis index lookups in the hot path.

Lines 65-66 call AXIS_KEYS.indexOf() inside the interval callback, which fires every 16ms when out of bounds. While the array is small (3 elements), this could be optimized with a lookup object computed once.

Create a lookup object outside the component:

const AXIS_KEYS = ['x', 'y', 'z'] as const
const AXIS_INDEX: Record<string, number> = { x: 0, y: 1, z: 2 }

Then replace the indexOf calls:

  return Array.isArray(v)
    ? {
-       [v1]: v[AXIS_KEYS.indexOf(v1 as 'x' | 'y' | 'z')] + incX,
-       [v2]: v[AXIS_KEYS.indexOf(v2 as 'x' | 'y' | 'z')] + incY,
+       [v1]: v[AXIS_INDEX[v1]] + incX,
+       [v2]: v[AXIS_INDEX[v2]] + incY,
      }
    : {
        [v1]: v[v1] + incX,
        [v2]: v[v2] + incY,
      }

63-72: Type detection could be more explicit.

Using Array.isArray(v) to distinguish Vector3d from Vector2d couples the code to implementation details (array vs object representation). Consider using a more explicit type guard or checking for a discriminant property.

Example approach:

// In the types file, add a type guard
function isVector3dArray(v: Vector2d | Vector3d): v is [number, number, number] {
  return Array.isArray(v)
}

Or check for specific properties:

return v1 in v && v2 in v
  ? {
      [v1]: v[v1] + incX,
      [v2]: v[v2] + incY,
    }
  : {
      [v1]: v[AXIS_INDEX[v1]] + incX,
      [v2]: v[AXIS_INDEX[v2]] + incY,
    }

136-136: Simplify the children fallback.

The ternary can be replaced with a more concise logical OR operator.

-           {children ? children : <JoystickGrid />}
+           {children || <JoystickGrid />}
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 801fd5a and cf37e86.

📒 Files selected for processing (3)
  • packages/leva/src/components/UI/Joystick.tsx (1 hunks)
  • packages/leva/src/components/UI/StyledJoystick3d.ts (1 hunks)
  • packages/leva/src/hooks/useKeyPress.ts (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • packages/leva/src/hooks/useKeyPress.ts
  • packages/leva/src/components/UI/StyledJoystick3d.ts
🧰 Additional context used
🧬 Code graph analysis (1)
packages/leva/src/components/UI/Joystick.tsx (4)
packages/leva/src/plugins/Vector2d/vector2d-types.ts (1)
  • Vector2dProps (7-7)
packages/leva/src/utils/event.ts (1)
  • multiplyStep (1-1)
packages/leva/src/components/UI/StyledJoystick.ts (3)
  • JoystickTrigger (3-24)
  • JoystickPlayground (26-54)
  • JoystickGrid (56-81)
packages/leva/src/components/UI/Misc.tsx (1)
  • Portal (7-14)
🔇 Additional comments (4)
packages/leva/src/components/UI/Joystick.tsx (4)

1-15: LGTM!

The imports, type definitions, and AXIS_KEYS constant are well-structured. Extracting AXIS_KEYS as a module-level constant avoids repeated allocations during render cycles.


17-29: LGTM!

The refs and state initialization follow React best practices. Using refs for timeout and multiplier values that don't need to trigger re-renders is the right choice.


39-51: LGTM!

Settings extraction and bounds calculation are clean and correct. The 0.8 factor aligns with the JoystickGrid styling (80% width/height).


82-93: LGTM!

The keyboard effect properly sets up global listeners for step multiplier adjustments. Cleanup is thorough, preventing memory leaks by removing listeners and clearing the interval.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

🧹 Nitpick comments (1)
packages/leva/src/components/UI/Joystick.tsx (1)

77-85: Guard array axis lookup during out-of-bounds updates

When v is an array, hitting a plane whose key we don’t recognise (or a future axis rename) makes AXIS_KEYS.indexOf(...) return -1, so v[-1] is undefined and the interval writes NaN. Since you already added getAxisValue, we can reuse it here to keep the array/object paths aligned and robust.

-        return Array.isArray(v)
-          ? {
-              [v1]: v[AXIS_KEYS.indexOf(v1 as 'x' | 'y' | 'z')] + incX,
-              [v2]: v[AXIS_KEYS.indexOf(v2 as 'x' | 'y' | 'z')] + incY,
-            }
-          : {
+        return Array.isArray(v)
+          ? {
+              [v1]: getAxisValue(v, v1) + incX,
+              [v2]: getAxisValue(v, v2) + incY,
+            }
+          : {
               [v1]: v[v1] + incX,
               [v2]: v[v2] + incY,
             }
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between cf37e86 and 324415b.

📒 Files selected for processing (1)
  • packages/leva/src/components/UI/Joystick.tsx (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
packages/leva/src/components/UI/Joystick.tsx (6)
packages/leva/src/plugins/Vector2d/vector2d-types.ts (1)
  • Vector2dProps (7-7)
packages/leva/src/utils/event.ts (1)
  • multiplyStep (1-1)
packages/leva/src/components/UI/StyledJoystick.ts (3)
  • JoystickTrigger (3-24)
  • JoystickPlayground (26-54)
  • JoystickGrid (56-81)
packages/leva/src/components/UI/Misc.tsx (1)
  • Portal (7-14)
packages/leva/src/plugins/Vector2d/Joystick.tsx (8)
  • Joystick (13-138)
  • delta (90-124)
  • joystickShown (27-33)
  • onUpdate (54-68)
  • timeout (49-69)
  • v (55-67)
  • window (83-87)
  • window (71-75)
packages/leva/src/plugins/Vector2d/Vector2d.tsx (1)
  • Vector2dComponent (20-31)

@s4l4x
Copy link
Author

s4l4x commented Nov 9, 2025

I went through the code suggestions. Please let me know if there's anything else needed

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