feat: PopoverPanel + Dropdown deprecation#566
Conversation
cb-ekuersch
left a comment
There was a problem hiding this comment.
Before we can recommend switching to Popover we should at least:
- audit Popover to make sure it can handle any/all use cases that Dropdown can. Any storybook stories from Dropdown should be able to be effortlessly migrated to Popover stories
- add docsite pages for Popover
ca7a9d9 to
1bcca8d
Compare
102cbda to
4f73334
Compare
✅ Heimdall Review Status
✅
|
| Code Owner | Status | Calculation | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|
| ui-systems-eng-team |
✅
1/1
|
Denominator calculation
|
5246936 to
5aa2102
Compare
5aa2102 to
e99745a
Compare
| import React, { memo, useCallback, useMemo } from 'react'; | ||
| import React, { forwardRef, memo, useCallback, useMemo } from 'react'; | ||
| import { zIndex } from '@coinbase/cds-common/tokens/zIndex'; | ||
| import { useMergeRefs } from '@coinbase/cds-common/hooks/useMergeRefs'; |
There was a problem hiding this comment.
will replace with mergeRefs after merging into v9
| type Placement as FloatingPlacement, | ||
| shift, | ||
| useFloating, | ||
| } from '@floating-ui/react-dom'; |
There was a problem hiding this comment.
originally replacing with popper with floating UI in popover was done in v9 feature branch, but moved it here since we needed to have PopoverPanel before v9 and Popover is affected
There was a problem hiding this comment.
non-breaking since all props are the same
| /** Can optionally pass a maxHeight. | ||
| * @default 300 | ||
| */ | ||
| maxPanelHeight?: React.CSSProperties['maxHeight']; |
There was a problem hiding this comment.
maxPanelWidth/Height instead of maxHeight to make it clear to which element dimension is set
| onClick={!disableOverlayPress ? onOverlayPress : undefined} | ||
| testID="modal-overlay" | ||
| /> | ||
| ) : ( |
There was a problem hiding this comment.
adding this here so Modal without overlay can still be closed when clicking outside
There was a problem hiding this comment.
Would moving this to the parent <Box> be a better alternative? Or does that not work?
There was a problem hiding this comment.
I tried that at first, but the problem with that was clicking inside the dialog would also close the modal since the parent Box wraps both the overlay and the dialog.
| /** | ||
| * @deprecated Import `useResponsiveHeight` from `@coinbase/cds-web/popover` instead. This will be removed in a future major release. | ||
| * @deprecationExpectedRemoval v10 | ||
| */ |
There was a problem hiding this comment.
added a replacement under popover directory. the replacement handle can handle maxHeight with responsive value
| onBlur?: (event?: React.FocusEvent) => void; | ||
| /** Callback fired when a mouse down event is fired on the subject */ | ||
| onMouseDown?: (event: React.MouseEvent) => void; | ||
| /** |
There was a problem hiding this comment.
added style can className support to Popover
maximo-macchi-cb
left a comment
There was a problem hiding this comment.
Currently reviewed new PopoverPanel components in web. Will finish review tomorrow but saving here just in case.
| title: 'Components/Overlay/PopoverPanel', | ||
| component: PopoverPanel, | ||
| parameters: { | ||
| a11y: { test: 'off' }, |
There was a problem hiding this comment.
Why are a11y tests being disabled? I enabled them locally and in the Storybook UI and CLI all tests pass
There was a problem hiding this comment.
oh my bad. I re-enabled it
| /** | ||
| * Width of the panel as a percentage string or number converted to pixels. | ||
| */ | ||
| panelWidth?: PopoverPanelContentBaseProps['width']; | ||
| /** Minimum width of the panel as a percentage string or number converted to pixels. */ | ||
| minPanelWidth?: PopoverPanelContentBaseProps['minWidth']; | ||
| /** Maximum width of the panel as a percentage string or number converted to pixels. */ | ||
| maxPanelWidth?: PopoverPanelContentBaseProps['maxWidth']; | ||
| /** Height of the panel as a percentage string or number converted to pixels. */ | ||
| panelHeight?: PopoverPanelContentBaseProps['height']; |
There was a problem hiding this comment.
Small nit: since our other height / width props only are named as "height" and "width" I think it'd be easier for engineers to pick up on by keeping the naming convention consistent
| /** | |
| * Width of the panel as a percentage string or number converted to pixels. | |
| */ | |
| panelWidth?: PopoverPanelContentBaseProps['width']; | |
| /** Minimum width of the panel as a percentage string or number converted to pixels. */ | |
| minPanelWidth?: PopoverPanelContentBaseProps['minWidth']; | |
| /** Maximum width of the panel as a percentage string or number converted to pixels. */ | |
| maxPanelWidth?: PopoverPanelContentBaseProps['maxWidth']; | |
| /** Height of the panel as a percentage string or number converted to pixels. */ | |
| panelHeight?: PopoverPanelContentBaseProps['height']; | |
| /** | |
| * Width of the panel as a percentage string or number converted to pixels. | |
| */ | |
| width?: PopoverPanelContentBaseProps['width']; | |
| /** Minimum width of the panel as a percentage string or number converted to pixels. */ | |
| minWidth?: PopoverPanelContentBaseProps['minWidth']; | |
| /** Maximum width of the panel as a percentage string or number converted to pixels. */ | |
| maxWidth?: PopoverPanelContentBaseProps['maxWidth']; | |
| /** Height of the panel as a percentage string or number converted to pixels. */ | |
| height?: PopoverPanelContentBaseProps['height']; |
There was a problem hiding this comment.
I added the panel prefix on purpose so it is clear that they define dimensions of the popover panel not the popover trigger.
| // eslint-disable-next-line jsx-a11y/click-events-have-key-events | ||
| <div | ||
| className={cx(block ? blockCss : triggerContainerCss, classNames?.triggerContainer)} | ||
| onBlur={onBlur} | ||
| onClick={disabled ? undefined : onOpen} | ||
| style={styles?.triggerContainer} | ||
| > |
There was a problem hiding this comment.
Even though this is meant for mobile devices, this could still render in a web browser on a desktop where the browser's window width is small, right? Two suggestions for this:
- An
onKeyDownhandler should be added here - A prop for
accessibilityRolewith default value of"button"should be added to the prop API and used here
| // eslint-disable-next-line jsx-a11y/click-events-have-key-events | |
| <div | |
| className={cx(block ? blockCss : triggerContainerCss, classNames?.triggerContainer)} | |
| onBlur={onBlur} | |
| onClick={disabled ? undefined : onOpen} | |
| style={styles?.triggerContainer} | |
| > | |
| <div | |
| accessibilityRole={accessibilityRole} | |
| className={cx(block ? blockCss : triggerContainerCss, classNames?.triggerContainer)} | |
| onBlur={onBlur} | |
| onClick={disabled ? undefined : onOpen} | |
| // onKeyDown could handle the key event then call onOpen | |
| onKeyDown={disabled ? undefined : onKeyDown} | |
| style={styles?.triggerContainer} | |
| > |
There was a problem hiding this comment.
the panel already opens when pressing enter while focusing on the children at the moment without onKeyDown and since this component handles the open and close logic internally and does not take onKeyDown prop, we don't need to pass onKeydown here.
for accessibilityRole, the child is most likely a button already, if we add additional role to the trigger wrapper here, then we end up with nested buttons.
I will mention in the docs that they should use always interactive element as the child(trigger) of PopoverPanel
| hideOverlay={!showOverlay} | ||
| onClick={handleCaptureEvents} | ||
| onOverlayPress={onClose} | ||
| testID="popover-panel-modal" |
There was a problem hiding this comment.
We should make the testID a prop with default of "popover-panel-modal" so customers can configure this as they'd like
There was a problem hiding this comment.
added the testID prop but did not assign the default id because we are actually not using it and since this is a new component so no one else is selecting the component using this id.
| <FocusTrap | ||
| onEscPress={onClose} | ||
| respectNegativeTabIndex={respectNegativeTabIndex} | ||
| restoreFocusOnUnmount={restoreFocusOnUnmount} | ||
| > |
There was a problem hiding this comment.
I think adding the following props from FocusTrapBaseProps would be beneficial for customers to configure:
disableAutoFocusincludeTriggerInFocusTrapfocusTabIndexElementsautoFocusDelay
| background="bg" | ||
| borderRadius={400} | ||
| className={cx(popoverPanelContentClassName, className)} | ||
| elevation={2} | ||
| minWidth={minWidth} | ||
| overflow="auto" |
There was a problem hiding this comment.
Shouldn't these be configurable? At least background, borderRadius, and overflow
cb-ekuersch
left a comment
There was a problem hiding this comment.
Looking good, but I have a couple of quesitons:
- What are the long term plans for the two popover directories: src/overlays/popover and src/popover?
- Are we planning to keep supporting Popover on its own? I thought PopoverPanel was meant to completely replace it rather than compose it
| PageHeader?: ConfigResolver<PageHeaderBaseProps>; | ||
| Pagination?: ConfigResolver<PaginationBaseProps>; | ||
| PopoverPanel?: ConfigResolver<PopoverPanelBaseProps>; | ||
| PopoverPanelContent?: ConfigResolver<PopoverPanelContentBaseProps>; |
There was a problem hiding this comment.
Customers never explicitly render a PopoverPanelContent component. It is an implementation detail of the content prop. I recommend we don't add "hidden" components to the component theme config
There was a problem hiding this comment.
got it. I'm gonna remove TooltipContent from the component theme config for consistency then.
| } | ||
|
|
||
| export const Default: Story = { | ||
| render: () => <DefaultStory />, |
There was a problem hiding this comment.
i think there is a shorter way to define these, take a moment to review: https://storybook.js.org/blog/storybook-csf3-is-here/
You can have a default export that defines a render and then export objects that change the props/args. Not sure if that setup would help you at all here
There was a problem hiding this comment.
removed this redundant code
exporting the DefaultStory component would do it.
| import { DefaultThemeProvider } from '../../../utils/test'; | ||
| import { PopoverPanel, type PopoverPanelRef } from '../PopoverPanel'; | ||
|
|
||
| jest.mock('react-use-measure'); |
There was a problem hiding this comment.
nit: i wonder if we should just centralize the mock of this if its always necessary to set up, similar to what we did for floating ui
There was a problem hiding this comment.
good suggestion! I will address this in a separate pr since we have a lot of tests that mock this hook.
| export * from './overlay/Overlay'; | ||
| export * from './popover/Popover'; | ||
| export * from './popover/PopoverPanel'; | ||
| export * from './popover/PopoverPanelContent'; |
There was a problem hiding this comment.
do we really want to to encourage direct use by putting it in the barrel file? It seems like this component is more an implementation detail; a true sub-component
There was a problem hiding this comment.
good catch. I removed the PopoverPanelContent and made it strictly internal
|
|
||
| export type PopoverPanelBaseProps = Pick< | ||
| PopoverBaseProps, | ||
| | 'children' |
There was a problem hiding this comment.
put children on props, not base props
| | 'controlledElementAccessibilityProps' | ||
| | 'respectNegativeTabIndex' | ||
| > & | ||
| SharedProps & |
There was a problem hiding this comment.
nit: put full types first in set
|
|
||
| export type PopoverPanelProps = PopoverPanelBaseProps & { | ||
| style?: React.CSSProperties; | ||
| styles?: { |
There was a problem hiding this comment.
use styles and classnames helper - maybe try the skill Hunter added to see how it performs
| visible: boolean; | ||
| }; | ||
|
|
||
| export const POPOVER_PANEL_MAX_HEIGHT = 300; |
There was a problem hiding this comment.
it might make more sense to encapsulate this in the module for the max height hook
There was a problem hiding this comment.
I ended up removing the max height hook. it is no longer necessary because floating-ui can already handle that for us.
| placement: 'bottom-start', | ||
| }; | ||
|
|
||
| function usePopoverPanelImperativeHandle( |
There was a problem hiding this comment.
nit: is this separate hook necessary?
There was a problem hiding this comment.
it is separate because we use it both in Mobile(Modal) and FloatingPopoverPanel sub components.
maximo-macchi-cb
left a comment
There was a problem hiding this comment.
Review fully done! Left a couple additional comments
| ); | ||
|
|
||
| /** | ||
| * @deprecated Use PopoverPanel instead. This will be removed in a future major release. |
There was a problem hiding this comment.
Select should be mentioned here too. Dropdown was very similar to Select. So while PopoverPanel may satisfy some customer's needs, others may be wanting a Select-like component and Select / SelectChip would be better for that than PopoverPanel
There was a problem hiding this comment.
updated all deprecation messages to recommend Select for selectable use case
| onClick={!disableOverlayPress ? onOverlayPress : undefined} | ||
| testID="modal-overlay" | ||
| /> | ||
| ) : ( |
There was a problem hiding this comment.
Would moving this to the parent <Box> be a better alternative? Or does that not work?
- Add PopoverPanel, PopoverPanelContent, tests, stories, and @coinbase/cds-web/popover export - Document PopoverPanel (docgen, sidebar, component docs) - Deprecate Dropdown, DropdownProps-related types, and DropdownContent for removal in v10 - Popover: forwardRef with mergeRefs for Floating UI reference; flatten to one root div; add style/className to PopoverProps - Re-export PopoverPanel from overlays index; add Dropdown docs warning
fix ModalWrapper not closing when clicked outside without overlay
f1e9bd2 to
37c0cb1
Compare
Good catch! I removed the src/popover directory. It was added by mistake. PopoverPanel and Popover are only exported from overlays now.
yes, we still need to support Popover on its own because I have seen consumers using this component in many places and implemented their own PopoverPanel/Dropdown logic on top of it. |
What changed? Why?
Popover (internal)
offset (skid/gap), flip + shift + limitShift for fixed placements, and autoPlacement
for placement values that start with "auto".
reference ref (useMergeRefs).
PopoverPanel / PopoverPanelContent
(closePopover) => node, optional enableMobileModal, sizing (panelWidth, min/max
width/height), showOverlay, contentPosition, ref with openPopover/closePopover.
core/componentConfig.ts.
(package.json exports).
styles explorer, sidebar, webMetadata).
useResponsivePanelMaxHeight (renamed from useResponsiveHeight in popover)
PopoverPanel).
(was dropdownHeight in the shared logic).
sizing; keep the same viewport / calc(100vh - …) responsive behavior.
phone/tablet/desktop breakpoints via useBreakpoints.
with the original non-responsive maxHeight API; deprecate in favor of importing
useResponsivePanelMaxHeight from @coinbase/cds-web/popover (removal v10) and add a
short comment that new behavior lives under overlays/popover.
Dropdown deprecation
Dropdown webMetadata warning: use PopoverPanel / PopoverPanelContent instead
(expected removal v10).
Other
receives overlay press so dismiss works without a visible Overlay.
Breaking / migration notes for consumers
ref and subject style/className are additive.
dropdown/useResponsiveHeight is legacy and deprecated.
UI changes
storybook
docs
Screen.Recording.2026-04-02.at.1.15.52.AM.mov
Testing
How has it been tested?
Testing instructions
Illustrations/Icons Checklist
Required if this PR changes files under
packages/illustrations/**orpackages/icons/**Change management
type=routine
risk=low
impact=sev5
automerge=false