Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions apps/docs/docgen.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,7 @@ module.exports = {
'overlays/modal/FullscreenModalLayout',
'overlays/modal/FullscreenModalHeader',
'overlays/overlay/Overlay',
'overlays/popover/PopoverPanel',
'overlays/PortalProvider',
'overlays/Toast',
'overlays/tray/Tray',
Expand Down
1 change: 1 addition & 0 deletions apps/docs/docs/components/layout/Dropdown/webMetadata.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
"storybook": "https://cds-storybook.coinbase.com/?path=/story/components-dropdown-dropdown--default",
"figma": "https://www.figma.com/design/k5CtyJccNQUGMI5bI4lJ2g/%E2%9C%A8-CDS-Components?m=auto&node-id=465-13742&t=8hgl6nX25pIzCo0y-1",
"description": "An overlay that opens and closes.",
"warning": "This component is deprecated. Please use PopoverPanel instead.",
"relatedComponents": [
{
"label": "Select",
Expand Down
225 changes: 225 additions & 0 deletions apps/docs/docs/components/overlay/PopoverPanel/_webExamples.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,225 @@
## Basics

Pass `content` for the panel body and `children` as the trigger. The trigger toggles open and closed on press; the panel applies focus management and escape-to-close behavior.

:::tip Use an interactive element as the trigger
Always pass a natively interactive element — such as [Button](/components/inputs/Button/), [IconButton](/components/inputs/IconButton/), or an `<a>` tag — as `children`. Non-interactive elements (plain `div`, `span`, `Text`) are not reachable by keyboard and are invisible to assistive technology, which breaks accessibility.
:::

```jsx live
function BasicExample() {
return (
<PortalProvider>
<PopoverPanel
content={({ closePopover }) => (
<VStack padding={3} gap={2}>
<Text font="headline">Panel title</Text>
<Text color="fgMuted">Arbitrary content for a floating panel.</Text>
<Button variant="secondary" compact onClick={closePopover}>
Action
</Button>
</VStack>
)}
accessibilityLabel="Example settings panel"
>
<Button>Open panel</Button>
</PopoverPanel>
</PortalProvider>
);
}
```

## Selectable list

Use [`ListCell`](/components/data-display/ListCell/) with local state for the selected row and `closePopover` from the `content` render callback. You do not need `SelectProvider` or `SelectContext`.

After a value is chosen, the trigger often shows only the title. Set **`accessibilityLabel`** on the trigger to include the same details a sighted user gets from the list (for example title and description). Optionally set the panel **`accessibilityLabel`** so the dialog name matches the task (first choice vs. changing the value).

```jsx live
function ListCellSelectExample() {
const [selectedId, setSelectedId] = useState(null);
const options = [
{ id: 'eth', title: 'Ethereum', description: 'Main network' },
{ id: 'base', title: 'Base', description: 'L2 network' },
{ id: 'sol', title: 'Solana', description: 'External wallet' },
];
const selected = options.find((o) => o.id === selectedId);

return (
<PortalProvider>
<PopoverPanel
panelWidth={320}
accessibilityLabel={selected ? 'Change network' : 'Choose network'}
content={({ closePopover }) => (
<VStack gap={0}>
{options.map((option) => (
<ListCell
key={option.id}
spacingVariant="condensed"
title={option.title}
description={option.description}
selected={selectedId === option.id}
onClick={() => {
setSelectedId(option.id);
closePopover();
}}
/>
))}
</VStack>
)}
>
<Button
endIcon="caretDown"
width={240}
accessibilityLabel={
selected
? `${selected.title}, ${selected.description}, click to change`
: 'Choose network'
}
>
{selected ? selected.title : 'Choose Network'}
</Button>
</PopoverPanel>
</PortalProvider>
);
}
```

## Overlay and placement

Use `showOverlay` to dim content behind the panel. Adjust floating placement with `contentPosition` (see [Floating UI placement](https://floating-ui.com/docs/useFloating#placement)).

```jsx live
function OverlayAndPlacementExample() {
return (
<PortalProvider>
<HStack gap={3} flexWrap="wrap">
<PopoverPanel
content={({ closePopover }) => (
<VStack padding={3} gap={2}>
<Text>Content with overlay and top placement.</Text>
<Button variant="secondary" compact onClick={closePopover}>
Done
</Button>
</VStack>
)}
showOverlay
accessibilityLabel="Panel with overlay"
>
<Button>With overlay</Button>
</PopoverPanel>
<PopoverPanel
content={({ closePopover }) => (
<VStack padding={3} gap={2}>
<Text>Content with overlay and top placement.</Text>
<Button variant="secondary" compact onClick={closePopover}>
Done
</Button>
</VStack>
)}
contentPosition={{ placement: 'top', gap: 1 }}
accessibilityLabel="Panel above trigger"
>
<Button>Top placement</Button>
</PopoverPanel>
</HStack>
</PortalProvider>
);
}
```

## Panel sizing

By default, the panel content uses the same width as the trigger. Set `panelWidth`, `minPanelWidth`, `maxPanelWidth`, and `maxPanelHeight` when you need different constraints. The default max height is exported as `POPOVER_PANEL_MAX_HEIGHT`.

```jsx live
function SizingExample() {
return (
<PortalProvider>
<PopoverPanel
content={({ closePopover }) => (
<VStack padding={2} gap={1}>
{Array.from({ length: 12 }, (_, i) => (
<Text key={i}>Row {i + 1}</Text>
))}
<Button variant="secondary" compact onClick={closePopover}>
Close
</Button>
</VStack>
)}
panelWidth={280}
maxPanelHeight={200}
accessibilityLabel="Scrollable panel"
>
<Button>Fixed width and max height</Button>
</PopoverPanel>
</PortalProvider>
);
}
```

## Mobile modal

On small viewports, pass `enableMobileModal` to render the panel in a modal shell instead of a floating popover.

```jsx live
function MobileModalExample() {
return (
<PortalProvider>
<PopoverPanel
content={({ closePopover }) => (
<VStack padding={3} gap={2}>
<Text font="headline">Modal-style panel</Text>
<Text color="fgMuted">
Useful when the floating surface would be cramped on phone breakpoints.
</Text>
<Button variant="secondary" compact onClick={closePopover}>
Close
</Button>
</VStack>
)}
enableMobileModal
accessibilityLabel="Settings in modal"
panelWidth={320}
maxPanelWidth="80vw"
>
<Button>Open (modal on small screens)</Button>
</PopoverPanel>
</PortalProvider>
);
}
```

## Imperative open and close

Use a ref to call `openPopover` and `closePopover` when you need to drive visibility from elsewhere (for example, a separate control or analytics callback).

```jsx live
function ImperativeExample() {
const panelRef = useRef(null);

return (
<PortalProvider>
<HStack gap={2} flexWrap="wrap" alignItems="center">
<Button variant="secondary" onClick={() => panelRef.current?.openPopover()}>
Open programmatically
</Button>
<PopoverPanel
ref={panelRef}
content={
<VStack padding={3} gap={2}>
<Text>Panel opened from an external button.</Text>
<Button variant="secondary" compact onClick={() => panelRef.current?.closePopover()}>
Close from inside
</Button>
</VStack>
}
accessibilityLabel="Programmatic panel"
>
<Button>Trigger</Button>
</PopoverPanel>
</HStack>
</PortalProvider>
);
}
```
10 changes: 10 additions & 0 deletions apps/docs/docs/components/overlay/PopoverPanel/_webPropsTable.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import ComponentPropsTable from '@site/src/components/page/ComponentPropsTable';
import webPropsData from ':docgen/web/overlays/popover/PopoverPanel/data';
import { sharedParentTypes } from ':docgen/_types/sharedParentTypes';
import { sharedTypeAliases } from ':docgen/_types/sharedTypeAliases';

<ComponentPropsTable
props={webPropsData}
sharedTypeAliases={sharedTypeAliases}
sharedParentTypes={sharedParentTypes}
/>
59 changes: 59 additions & 0 deletions apps/docs/docs/components/overlay/PopoverPanel/_webStyles.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import { useEffect, useRef } from 'react';
import { ComponentStylesTable } from '@site/src/components/page/ComponentStylesTable';
import { StylesExplorer } from '@site/src/components/page/StylesExplorer';
import { Button } from '@coinbase/cds-web/buttons';
import { VStack } from '@coinbase/cds-web/layout';
import { Text } from '@coinbase/cds-web/typography';
import { PortalProvider } from '@coinbase/cds-web/overlays';
import { PopoverPanel } from '@coinbase/cds-web/overlays';

import webStylesData from ':docgen/web/overlays/popover/PopoverPanel/styles-data';

export const PopoverPanelStylesExample = ({ classNames }) => {
const panelRef = useRef(null);

useEffect(() => {
if (classNames.content) {
panelRef.current?.openPopover();
} else {
panelRef.current?.closePopover();
}

}, [classNames]);

return (

<PortalProvider>
<VStack gap={2}>
<Text color="fgMuted" font="label2">
Select a selector to highlight it. The panel opens automatically.
</Text>
<PopoverPanel
ref={panelRef}
accessibilityLabel="Styles explorer"
classNames={classNames}
enableMobileModal
content={
<VStack gap={2} padding={3}>
<Text>Panel body</Text>
<Button compact onClick={() => panelRef.current?.closePopover()} variant="secondary">
Close
</Button>
</VStack>
}
>
<Button>Open trigger</Button>
</PopoverPanel>
</VStack>
</PortalProvider>
); };

## Explorer

<StylesExplorer selectors={webStylesData.selectors}>
{(classNames) => <PopoverPanelStylesExample classNames={classNames} />}
</StylesExplorer>

## Selectors

<ComponentStylesTable componentName="PopoverPanel" styles={webStylesData} />
28 changes: 28 additions & 0 deletions apps/docs/docs/components/overlay/PopoverPanel/index.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
---
id: popoverPanel
title: PopoverPanel
platform_switcher_options: { web: true, mobile: false }
hide_title: true
---

import { VStack } from '@coinbase/cds-web/layout';
import { ComponentHeader } from '@site/src/components/page/ComponentHeader';
import { ComponentTabsContainer } from '@site/src/components/page/ComponentTabsContainer';

import webPropsToc from ':docgen/web/overlays/popover/PopoverPanel/toc-props';
import WebPropsTable from './_webPropsTable.mdx';
import WebStyles, { toc as webStylesToc } from './_webStyles.mdx';
import WebExamples, { toc as webExamplesToc } from './_webExamples.mdx';
import webMetadata from './webMetadata.json';

<VStack gap={5}>
<ComponentHeader title="PopoverPanel" webMetadata={webMetadata} />
<ComponentTabsContainer
webPropsTable={<WebPropsTable />}
webExamples={<WebExamples />}
webExamplesToc={webExamplesToc}
webPropsToc={webPropsToc}
webStyles={<WebStyles />}
webStylesToc={webStylesToc}
/>
</VStack>
30 changes: 30 additions & 0 deletions apps/docs/docs/components/overlay/PopoverPanel/webMetadata.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
{
"import": "import { PopoverPanel } from '@coinbase/cds-web/overlays'",
"source": "https://github.com/coinbase/cds/blob/master/packages/web/src/overlays/popover/PopoverPanel.tsx",
"storybook": "https://cds-storybook.coinbase.com/?path=/story/components-overlay-popoverpanel--default",
"description": "PopoverPanel anchors an elevated floating panel to a trigger element.",
"relatedComponents": [
{
"label": "Modal",
"url": "/components/overlay/Modal/"
},
{
"label": "Tooltip",
"url": "/components/overlay/Tooltip/"
},
{
"label": "FocusTrap",
"url": "/components/overlay/FocusTrap/"
}
],
"dependencies": [
{
"name": "framer-motion",
"version": "^10.18.0"
},
{
"name": "react-dom",
"version": "^18.3.1"
}
]
}
5 changes: 5 additions & 0 deletions apps/docs/sidebars.ts
Original file line number Diff line number Diff line change
Expand Up @@ -485,6 +485,11 @@ const sidebars: SidebarsConfig = {
id: 'components/overlay/Overlay/overlay',
label: 'Overlay',
},
{
type: 'doc',
id: 'components/overlay/PopoverPanel/popoverPanel',
label: 'PopoverPanel',
},
{
type: 'doc',
id: 'components/overlay/PortalProvider/portalProvider',
Expand Down
4 changes: 4 additions & 0 deletions packages/common/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@ All notable changes to this project will be documented in this file.

<!-- template-start -->

## 8.66.0 ((4/16/2026, 01:57 PM PST))

This is an artificial version bump with no new change.

## 8.65.0 ((4/16/2026, 10:06 AM PST))

This is an artificial version bump with no new change.
Expand Down
Loading
Loading