Skip to content

[JEWEL-1029] [JEWEL-1230] Reimplement Dropdown component - #3409

Open
Wellington Costa (wellingtoncosta) wants to merge 1 commit into
JetBrains:masterfrom
wellingtoncosta:wp/reimplement-dropdown-component
Open

[JEWEL-1029] [JEWEL-1230] Reimplement Dropdown component#3409
Wellington Costa (wellingtoncosta) wants to merge 1 commit into
JetBrains:masterfrom
wellingtoncosta:wp/reimplement-dropdown-component

Conversation

@wellingtoncosta

@wellingtoncosta Wellington Costa (wellingtoncosta) commented Feb 5, 2026

Copy link
Copy Markdown
Collaborator

Summary

This PR introduces a significant refactoring of PopupMenu to use PopupContainer instead of directly using Popup, consolidating visual styling (shadows, borders, backgrounds) and scrolling logic in one place. Previously, MenuContent had its own implementation of these features; now PopupContainer handles all popup styling consistently across the application, with MenuContent focusing solely on rendering menu items. Both menu content and ad content are now in the same scrollable area within PopupContainer.

To support this refactoring, PopupContainer was enhanced with scrolling support via maxHeight, useIntrinsicWidth for width control, and key event handling. The PopupMenu API now accepts both menuStyle and popupContainerStyle parameters to better separate menu item styling from container styling.

Additionally, this PR introduces MenuComboBox, a modern replacement for the deprecated Dropdown component that combines ComboBox UI with MenuContent for richer popup functionality including icons, keybindings, separators, and submenus. The component provides better keyboard navigation, focus management, and accessibility support.

Showcase samples were updated to demonstrate MenuComboBox usage.

Screen recording

Screen.Recording.2026-02-05.at.11.51.22.AM.mov

Release notes

⚠️ Important Changes

  • JEWEL-1230 Dropdown component is now deprecated in favor of MenuComboBox

New features

  • JEWEL-1029 Added scrolling support to PopupContainer with optional maxHeight parameter and vertical scrollbar
  • JEWEL-1029 Added useIntrinsicWidth parameter to PopupContainer for controlling width measurement (set to false for SubcomposeLayout-based components like LazyColumn)
  • JEWEL-1029 Added key event handling to PopupContainer via onPreviewKeyEvent and onKeyEvent parameters
  • JEWEL-1029 Added PopupMenu overloads accepting both menuStyle and popupContainerStyle for better separation of menu item styling from container styling
  • JEWEL-1230 Introduced experimental MenuComboBox as modern Dropdown replacement with support for menu items with icons, keybindings, separators, and submenus

Deprecated API

  • JEWEL-1230 Deprecated Dropdown in favor of MenuComboBox with DeprecationLevel.WARNING
  • JEWEL-1029 Deprecated older PopupContainer, PopupMenu, and ComboBox overloads in favor of new variants with enhanced parameters

Note

Medium Risk
Moderate risk due to new/deprecated public UI APIs and changes to popup sizing/scrolling/focus behavior that could affect menus and combo boxes across the library.

Overview
Introduces experimental MenuComboBox (menu-backed dropdown) built on ComboBox + MenuContent, and deprecates the older Dropdown API with an automated ReplaceWith.

Refactors PopupMenu to render via PopupContainer (instead of raw Popup), splitting menuStyle vs popupContainerStyle and adding maxHeight-based scrolling support; MenuContent is simplified to item rendering only.

Extends PopupContainer with useIntrinsicWidth, maxHeight, and key-event hooks, and extends experimental ComboBox to accept PopupProperties, popup key handling, and intrinsic-width control; updates ListComboBox popup focusability handling and fixes stale onPopupVisibleChange captures.

Updates the showcase to demonstrate MenuComboBox (icons, separators, submenus, disabled state) and refreshes API dumps accordingly.

Written by Cursor Bugbot for commit d2c6822. This will update automatically on new commits. Configure here.

Comment thread platform/jewel/ui/src/main/kotlin/org/jetbrains/jewel/ui/component/Menu.kt Outdated

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.

Awesome stuff! 🚀

Just some minor changes + a little nit-picking 😈

Comment thread platform/jewel/ui/src/main/kotlin/org/jetbrains/jewel/ui/component/ComboBox.kt Outdated
Comment thread platform/jewel/ui/src/main/kotlin/org/jetbrains/jewel/ui/component/ComboBox.kt Outdated
Comment thread platform/jewel/ui/src/main/kotlin/org/jetbrains/jewel/ui/component/ComboBox.kt Outdated
@wellingtoncosta

Copy link
Copy Markdown
Collaborator Author

Daniel Bertoldi (@DanielSouzaBertoldi) I resolved all your comments (I hope so), so please take a look on this PR again once you have a chance. Let me know if you have more questions or concerns.

@wellingtoncosta
Wellington Costa (wellingtoncosta) force-pushed the wp/reimplement-dropdown-component branch 2 times, most recently from d9be05c to 96b26ca Compare February 9, 2026 23:55
@DanielSouzaBertoldi

Copy link
Copy Markdown
Collaborator

Wellington Costa (@wellingtoncosta) I noticed some changes are still pending. Let me know once you reply to my comments/add the changes, please

showIcons = anyItemHasIcon,
showKeybindings = anyItemHasKeybinding,
selectedSubMenu = selectedSubMenu,
setSelectedSubMenu = { selectedSubMenu = it },

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

ContextMenu loses all visual container styling

High Severity

The MenuContent function was refactored to remove all container styling (shadow, border, background, rounded corners, intrinsic width, scrolling, and scrollbar), moving those responsibilities to PopupContainer. However, ContextMenu still calls MenuContent directly inside a raw Popup without PopupContainer. This means all right-click context menus rendered through ContextMenuRepresentation will appear as unstyled floating items with no background, border, shadow, or scroll support.

Fix in Cursor Fix in Web

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.

Oh just saw Cursor also commented the same lol

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.

Awesome work! 🥳

Comment thread platform/jewel/ui/src/main/kotlin/org/jetbrains/jewel/ui/component/Menu.kt Outdated
showIcons = anyItemHasIcon,
showKeybindings = anyItemHasKeybinding,
selectedSubMenu = selectedSubMenu,
setSelectedSubMenu = { selectedSubMenu = it },

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.

Oh just saw Cursor also commented the same lol

@cursor cursor 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.

Cursor Bugbot has reviewed your changes and found 2 potential issues.

Fix All in Cursor

Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

true
}
)
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

MenuComboBox doesn't close popup on item selection

Medium Severity

The menuController in MenuComboBox is created via remember without keys, but the onDismissRequest lambda it captures closes the popup via popupManager. The menuController.closeAll() (called by MenuItemBase on item click) invokes onDismissRequest which returns true, correctly closing the root. However, the menuController doesn't receive the InputMode from closeAll — it uses a fixed lambda. The real concern is that menuController is not keyed on popupManager, so if popupManager were ever recreated (it won't in this case since it's also remember-ed), it would capture a stale reference. This is safe as-is but fragile.

Fix in Cursor Fix in Web

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.

If popupManager was keyed on another variable then the stale callback thing would have a chance to happen, I'll give you that.

However, keying menuController to popupManager would basically be a no-op. popupManager is remembered without keys so it's stable for the entire lifetime of the composable.

@cursor

cursor Bot commented Mar 16, 2026

Copy link
Copy Markdown

You have used all of your free Bugbot PR reviews.

To receive reviews on all of your PRs, visit the Cursor dashboard to activate Pro and start your 14-day free trial.

@DanielSouzaBertoldi
Daniel Bertoldi (DanielSouzaBertoldi) force-pushed the wp/reimplement-dropdown-component branch 2 times, most recently from 9140ac4 to 5c5b364 Compare March 19, 2026 18:01
@rock3r

Copy link
Copy Markdown
Collaborator

bugbot run

) {
// To avoid source/binary breaking change for clients that read this property
@Deprecated(message = "Use PopupContainerStyle.metrics.menuMargin instead.", level = DeprecationLevel.WARNING)
public val menuMargin: PaddingValues = PaddingValues()

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.

This preserves the old menuMargin symbol, which is great for compat, but not the old value, which may be a compat issue. The hidden constructor still accepts menuMargin, and existing compiled callers may also read menuStyle.metrics.menuMargin, but now always see PaddingValues() instead of the value they provided. This can lead to unexpected behaviours.

Can we keep storing the old value for compatibility, even if new code should use PopupContainerStyle.metrics.menuMargin and this value is effectively never used? In any case, I reckon the deprecation message/KDoc should make that explicit.

}

val needsScrolling = maxHeight != Dp.Unspecified
val scrollState = if (needsScrolling || useIntrinsicWidth) rememberScrollState() else null

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.

Wouldn't this make intrinsic-width popups scrollable even when no maxHeight was requested? PopupMenu, ContextMenu, MenuComboBox, and submenus all use intrinsic width, so they all get verticalScroll/scrollbar wiring even with maxHeight = Dp.Unspecified.

I think intrinsic width and scrolling should stay separate: use the intrinsic-width layout branch if needed, but only create/apply the scroll state when needsScrolling is true.

Note

This is an AI finding, I did not manage to confirm it, but seems legit?

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.

Yes, I do think this is a valid point. useIntrinsicWidth should only expand the container to fit its content 🤔 if we were applying an HorizontallyScrollbar then that'd be a different story, but that's not the case here. Scroll should only be applied when the height of the popup is not enough to display everything it has to

.onClick { popupManager.setPopupVisible(false) },
horizontalAlignment = horizontalPopupAlignment,
popupProperties = PopupProperties(focusable = false),
useIntrinsicWidth = useIntrinsicPopupWidth,

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.

Shouldn't we also pass the resolved maxHeight to PopupContainer(maxHeight = ...) here? Right now ComboBoxImpl caps the outer modifier with heightIn(max = maxHeight), but the new scrolling contract lives in PopupContainer.maxHeight.

This is currently masked a bit by intrinsic width also creating a scroll state, but if that is fixed, MenuComboBox(maxPopupHeight = ...) with non-lazy MenuContent would be height-capped without the container knowing it should make the content scrollable.

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.

Not sure if this is indeed how it works, but it seems to me this may be missing the max height; this call caps the popup height via splitButtonPopupModifier.heightIn(max = maxPopupHeight), but the menu branch doesn't pass maxHeight to PopupMenu, so PopupContainer doesn't own scrolling for overflowing menu content.

Can we pass maxHeight = maxPopupHeight here?

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.

Yes sir 🫡 this is basically the same fix as the one before this comment. Thanks!

@rock3r Sebastiano Poggi (rock3r) 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, all my concerns have been addressed.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants