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
5 changes: 5 additions & 0 deletions .changeset/responsive-anchored-overlay.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@primer/react": minor
---

AnchoredOverlay: Add prop to set responsive variant. Example: `variant: {regular: 'anchored', narrow: 'anchored'}`
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
20 changes: 20 additions & 0 deletions e2e/components/SelectPanel.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -108,4 +108,24 @@ test.describe('SelectPanel', () => {
`SelectPanel-Default-forced-colors-dark-modern-action-list--true.png`,
)
})

test(`Default @vrt responsive width .modern-action-list--true`, async ({page}) => {
await visit(page, {
id: 'components-selectpanel--default',
globals: {featureFlags: {primer_react_select_panel_with_modern_action_list: true}},
})

await page.setViewportSize({width: 767, height: 767})

// Open select panel
const isPanelOpen = await page.isVisible('[role="listbox"]')
if (!isPanelOpen) {
await page.keyboard.press('Tab')
await page.keyboard.press('Enter')
}

expect(await page.screenshot({animations: 'disabled'})).toMatchSnapshot(
`SelectPanel-Default-responsive-width-light-modern-action-list--true.png`,
)
})
})
10 changes: 9 additions & 1 deletion packages/react/src/AnchoredOverlay/AnchoredOverlay.docs.json
Original file line number Diff line number Diff line change
Expand Up @@ -147,12 +147,20 @@
"required": false,
"description": "",
"defaultValue": ""
}, {
},
{
"name": "pinPosition",
"type": "boolean",
"required": false,
"description": "If true, the overlay will attempt to prevent position shifting when sitting at the top of the anchor.",
"defaultValue": "false"
},
{
"name": "variant",
"type": "{ regular?: 'anchored', narrow?: 'anchored' | 'fullscreen' }",
"required": false,
"description": "Optional prop to set variant for narrow screen sizes",
"defaultValue": "{ regular: 'anchored', narrow: 'anchored' }"
}
]
}
13 changes: 11 additions & 2 deletions packages/react/src/AnchoredOverlay/AnchoredOverlay.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {useFocusZone} from '../hooks/useFocusZone'
import {useAnchoredPosition, useProvidedRefOrCreate, useRenderForcingRef} from '../hooks'
import {useId} from '../hooks/useId'
import type {PositionSettings} from '@primer/behaviors'
import {useResponsiveValue, type ResponsiveValue} from '../hooks/useResponsiveValue'

interface AnchoredOverlayPropsWithAnchor {
/**
Expand Down Expand Up @@ -93,6 +94,10 @@ interface AnchoredOverlayBaseProps extends Pick<OverlayProps, 'height' | 'width'
* If true, the overlay will attempt to prevent position shifting when sitting at the top of the anchor.
*/
pinPosition?: boolean
/**
* Optional prop to set variant for narrow screen sizes
*/
variant?: ResponsiveValue<'anchored', 'anchored' | 'fullscreen'>
}

export type AnchoredOverlayProps = AnchoredOverlayBaseProps &
Expand Down Expand Up @@ -122,6 +127,7 @@ export const AnchoredOverlay: React.FC<React.PropsWithChildren<AnchoredOverlayPr
anchorOffset,
className,
pinPosition,
variant = {regular: 'anchored', narrow: 'anchored'},
Copy link
Member

Choose a reason for hiding this comment

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

nit: should we support wide as well to follow suit with other components? or is there precedence in PRC for doing only some viewports? 🤔

Copy link
Member Author

@siddharthkp siddharthkp Mar 12, 2025

Choose a reason for hiding this comment

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

You're right, we should include it as well. (I think the default works even if you don't because it's all anchored)

Update: thinking about this a bit more, I think it depends on what we want to encourage. We probably don't want folks to use full-screen in wide given regular is fixed at anchored 🤔

We would need to revisit this decision if we ever add a wide-friendly variant like sidebar-panel

preventOverflow = true,
}) => {
const anchorRef = useProvidedRefOrCreate(externalAnchorRef)
Expand Down Expand Up @@ -183,6 +189,8 @@ export const AnchoredOverlay: React.FC<React.PropsWithChildren<AnchoredOverlayPr
})
useFocusTrap({containerRef: overlayRef, disabled: !open || !position, ...focusTrapSettings})

const currentResponsiveVariant = useResponsiveValue(variant, 'anchored')

return (
<>
{renderAnchor &&
Expand All @@ -206,8 +214,9 @@ export const AnchoredOverlay: React.FC<React.PropsWithChildren<AnchoredOverlayPr
visibility={position ? 'visible' : 'hidden'}
height={height}
width={width}
top={position?.top || 0}
left={position?.left || 0}
top={currentResponsiveVariant === 'anchored' ? position?.top || 0 : undefined}
left={currentResponsiveVariant === 'anchored' ? position?.left || 0 : undefined}
data-variant={currentResponsiveVariant}
anchorSide={position?.anchorSide}
className={className}
preventOverflow={preventOverflow}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@ import {ConfirmationDialog, useConfirm} from './ConfirmationDialog'
import theme from '../theme'
import {ThemeProvider} from '../ThemeProvider'
import {behavesAsComponent, checkExports} from '../utils/testing'
import {setupMatchMedia} from '../utils/test-helpers'

setupMatchMedia() // need to mock media for deprecated/ActionMenu

const Basic = ({confirmButtonType}: Pick<React.ComponentProps<typeof ConfirmationDialog>, 'confirmButtonType'>) => {
const [isOpen, setIsOpen] = useState(false)
Expand Down
9 changes: 9 additions & 0 deletions packages/react/src/Overlay/Overlay.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -156,4 +156,13 @@
&:where([data-visibility-hidden]) {
visibility: hidden;
}

&:where([data-variant='fullscreen']) {
top: 0;
left: 0;
width: 100vw;
height: 100vh;
margin: 0;
border-radius: unset;
}
}
3 changes: 3 additions & 0 deletions packages/react/src/Overlay/Overlay.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@ import BaseStyles from '../BaseStyles'
import {ThemeProvider} from '../ThemeProvider'
import {NestedOverlays, MemexNestedOverlays, MemexIssueOverlay, PositionedOverlays} from './Overlay.features.stories'
import {FeatureFlags} from '../FeatureFlags'
import {setupMatchMedia} from '../utils/test-helpers'

setupMatchMedia()

type TestComponentSettings = {
initialFocus?: 'button'
Expand Down
9 changes: 9 additions & 0 deletions packages/react/src/Overlay/Overlay.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,15 @@ const StyledOverlay = toggleStyledComponent(
max-width: calc(100vw - 2rem);
}

&:where([data-variant='fullscreen']) {
top: 0;
left: 0;
width: 100vw;
height: 100vh;
margin: 0;
border-radius: unset;
}

${sx};
`,
)
Expand Down
3 changes: 3 additions & 0 deletions packages/react/src/SelectPanel/SelectPanel.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@ import {getLiveRegion} from '../utils/testing'
import {IconButton} from '../Button'
import {ArrowLeftIcon} from '@primer/octicons-react'
import Box from '../Box'
import {setupMatchMedia} from '../utils/test-helpers'

setupMatchMedia()

const renderWithFlag = (children: React.ReactNode, flag: boolean) => {
return render(
Expand Down
3 changes: 3 additions & 0 deletions packages/react/src/TooltipV2/__tests__/Tooltip.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ import {render as HTMLRender} from '@testing-library/react'
import theme from '../../theme'
import {Button, IconButton, ActionMenu, ActionList, ThemeProvider, BaseStyles, ButtonGroup} from '../..'
import {XIcon} from '@primer/octicons-react'
import {setupMatchMedia} from '../../utils/test-helpers'

setupMatchMedia()

const TooltipComponent = (props: Omit<TooltipProps, 'text'> & {text?: string}) => (
<Tooltip text="Tooltip text" {...props}>
Expand Down
3 changes: 3 additions & 0 deletions packages/react/src/__tests__/ActionMenu.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@ import {behavesAsComponent, checkExports} from '../utils/testing'
import {SingleSelect} from '../ActionMenu/ActionMenu.features.stories'
import {MixedSelection} from '../ActionMenu/ActionMenu.examples.stories'
import {SearchIcon, KebabHorizontalIcon} from '@primer/octicons-react'
import {setupMatchMedia} from '../utils/test-helpers'

setupMatchMedia()

function Example(): JSX.Element {
return (
Expand Down
3 changes: 3 additions & 0 deletions packages/react/src/__tests__/AnchoredOverlay.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ import {Button} from '../Button'
import theme from '../theme'
import BaseStyles from '../BaseStyles'
import {ThemeProvider} from '../ThemeProvider'
import {setupMatchMedia} from '../utils/test-helpers'

setupMatchMedia()

type TestComponentSettings = {
initiallyOpen?: boolean
Expand Down
3 changes: 3 additions & 0 deletions packages/react/src/__tests__/LabelGroup.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ import {LabelGroup, Label, ThemeProvider, BaseStyles} from '..'
import {behavesAsComponent, checkExports} from '../utils/testing'
import theme from '../theme'
import userEvent from '@testing-library/user-event'
import {setupMatchMedia} from '../utils/test-helpers'

setupMatchMedia()

const ThemeAndStyleContainer: React.FC<React.PropsWithChildren> = ({children}) => (
<ThemeProvider theme={theme}>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,15 @@ exports[`AnchoredOverlay should render consistently when open 1`] = `
max-width: calc(100vw - 2rem);
}

.c1:where([data-variant='fullscreen']) {
top: 0;
left: 0;
width: 100vw;
height: 100vh;
margin: 0;
border-radius: unset;
}

@media (forced-colors:active) {
.c1 {
outline: solid 1px transparent;
Expand Down Expand Up @@ -82,6 +91,7 @@ exports[`AnchoredOverlay should render consistently when open 1`] = `
<div
class="c1"
data-focus-trap="active"
data-variant="anchored"
height="auto"
role="none"
style="left: 0px; top: 4px; --styled-overlay-visibility: visible;"
Expand Down
3 changes: 3 additions & 0 deletions packages/react/src/__tests__/deprecated/ActionMenu.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ import {ActionMenu} from '../../deprecated'
import {behavesAsComponent, checkExports} from '../../utils/testing'
import {BaseStyles, ThemeProvider} from '../..'
import type {ItemProps} from '../../deprecated/ActionList/Item'
import {setupMatchMedia} from '../../utils/test-helpers'

setupMatchMedia()

const items = [
{text: 'New file'},
Expand Down
20 changes: 20 additions & 0 deletions packages/react/src/utils/test-helpers.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -47,3 +47,23 @@ if (typeof document !== 'undefined') {
if (global.Element.prototype.scrollIntoView === undefined) {
global.Element.prototype.scrollIntoView = jest.fn()
}

// setup match media for tests that use useResponsiveValue or use a compone that uses useResponsiveValue
// we don't set this up globally for all tests because some tests need to be able mock matchMedia with more granular control
export const setupMatchMedia = () => {
// window.matchMedia() is not implemented by JSDOM so we have to create a mock:
// https://jestjs.io/docs/manual-mocks#mocking-methods-which-are-not-implemented-in-jsdom
Object.defineProperty(window, 'matchMedia', {
writable: true,
value: jest.fn().mockImplementation(query => ({
matches: false,
media: query,
onchange: null,
addListener: jest.fn(), // deprecated
removeListener: jest.fn(), // deprecated
addEventListener: jest.fn(),
removeEventListener: jest.fn(),
dispatchEvent: jest.fn(),
})),
})
}
Loading