diff --git a/.changeset/select-panel-display-in-viewport.md b/.changeset/select-panel-display-in-viewport.md
new file mode 100644
index 00000000000..05357d80c04
--- /dev/null
+++ b/.changeset/select-panel-display-in-viewport.md
@@ -0,0 +1,5 @@
+---
+'@primer/react': minor
+---
+
+SelectPanel: Add `displayInViewport` prop
diff --git a/packages/react/src/SelectPanel/SelectPanel.docs.json b/packages/react/src/SelectPanel/SelectPanel.docs.json
index f55961d2614..d7f91b944e1 100644
--- a/packages/react/src/SelectPanel/SelectPanel.docs.json
+++ b/packages/react/src/SelectPanel/SelectPanel.docs.json
@@ -237,6 +237,12 @@
"type": "boolean",
"defaultValue": "false",
"description": "If true, enables client-side list virtualization. Only the visible items plus a small overscan buffer are rendered in the DOM, dramatically improving performance for large lists. Recommended for lists with more than 100 items. Has no effect when `groupMetadata` is provided."
+ },
+ {
+ "name": "displayInViewport",
+ "type": "boolean",
+ "defaultValue": "false",
+ "description": "If true, the panel will attempt to fit entirely into the visible viewport, without having to scroll."
}
],
"subcomponents": []
diff --git a/packages/react/src/SelectPanel/SelectPanel.test.tsx b/packages/react/src/SelectPanel/SelectPanel.test.tsx
index fae621080c7..de52939b1ac 100644
--- a/packages/react/src/SelectPanel/SelectPanel.test.tsx
+++ b/packages/react/src/SelectPanel/SelectPanel.test.tsx
@@ -10,6 +10,29 @@ import {IconButton} from '../Button'
import {ArrowLeftIcon} from '@primer/octicons-react'
import classes from './SelectPanel.test.module.css'
import {implementsClassName} from '../utils/testing'
+import {getAnchoredPosition} from '@primer/behaviors'
+import type {AnchorPosition} from '@primer/behaviors'
+
+// Mock getAnchoredPosition to verify displayInViewport is forwarded
+vi.mock('@primer/behaviors', async () => {
+ const actual = await vi.importActual('@primer/behaviors')
+ return {
+ ...actual,
+ getAnchoredPosition: vi.fn(
+ (
+ _floatingElement: Element,
+ _anchorElement: Element | DOMRect,
+ _settings?: Partial<{displayInViewport?: boolean}>,
+ ) =>
+ ({
+ top: 100,
+ left: 100,
+ anchorSide: 'outside-bottom',
+ anchorAlign: 'start',
+ }) as AnchorPosition,
+ ),
+ }
+})
// Instead of importing from live-region/__tests__/test-helpers.ts, we define our own getLiveRegion function
export function getLiveRegion(): LiveRegionElement {
@@ -1734,3 +1757,49 @@ for (const usingRemoveActiveDescendant of [false, true]) {
})
})
}
+
+describe('SelectPanel displayInViewport prop', () => {
+ const mockGetAnchoredPosition = vi.mocked(getAnchoredPosition)
+
+ beforeEach(() => {
+ mockGetAnchoredPosition.mockClear()
+ })
+
+ it('should forward displayInViewport={true} to getAnchoredPosition', async () => {
+ const user = userEvent.setup()
+ render()
+
+ await user.click(screen.getByRole('button', {name: 'Select items'}))
+
+ await waitFor(() => {
+ expect(screen.getByRole('listbox')).toBeInTheDocument()
+ })
+
+ await waitFor(() => {
+ expect(mockGetAnchoredPosition).toHaveBeenCalled()
+ })
+
+ const calls = mockGetAnchoredPosition.mock.calls
+ const lastCall = calls[calls.length - 1]
+ expect(lastCall[2]?.displayInViewport).toBe(true)
+ })
+
+ it('should not set displayInViewport when prop is not provided', async () => {
+ const user = userEvent.setup()
+ render()
+
+ await user.click(screen.getByRole('button', {name: 'Select items'}))
+
+ await waitFor(() => {
+ expect(screen.getByRole('listbox')).toBeInTheDocument()
+ })
+
+ await waitFor(() => {
+ expect(mockGetAnchoredPosition).toHaveBeenCalled()
+ })
+
+ const calls = mockGetAnchoredPosition.mock.calls
+ const lastCall = calls[calls.length - 1]
+ expect(lastCall[2]?.displayInViewport).not.toBe(true)
+ })
+})
diff --git a/packages/react/src/SelectPanel/SelectPanel.tsx b/packages/react/src/SelectPanel/SelectPanel.tsx
index 82cd0812d1b..04667503618 100644
--- a/packages/react/src/SelectPanel/SelectPanel.tsx
+++ b/packages/react/src/SelectPanel/SelectPanel.tsx
@@ -131,7 +131,7 @@ type SelectPanelVariantProps = {variant?: 'anchored'; onCancel?: () => void} | {
export type SelectPanelProps = SelectPanelBaseProps &
Omit &
- Pick &
+ Pick &
AnchoredOverlayWrapperAnchorProps &
(SelectPanelSingleSelection | SelectPanelMultiSelection) &
SelectPanelVariantProps
@@ -203,6 +203,7 @@ function Panel({
showSelectAll = false,
focusPrependedElements,
virtualized,
+ displayInViewport,
...listProps
}: SelectPanelProps): JSX.Element {
const titleId = useId()
@@ -876,6 +877,7 @@ function Panel({
className={classes.Overlay}
displayCloseButton={showXCloseIcon}
closeButtonProps={closeButtonProps}
+ displayInViewport={displayInViewport}
>