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} >