diff --git a/.changeset/nine-buttons-lose.md b/.changeset/nine-buttons-lose.md deleted file mode 100644 index 69945bd669e..00000000000 --- a/.changeset/nine-buttons-lose.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@primer/react': minor ---- - -AnchoredOverlay: Add CSS Anchor Positioning to `AnchoredOverlay` (under a feature flag) diff --git a/.playwright/snapshots/components/AnchoredOverlay.test.ts-snapshots/AnchoredOverlay-Anchor-Alignment-light-css-anchor-positioning-linux.png b/.playwright/snapshots/components/AnchoredOverlay.test.ts-snapshots/AnchoredOverlay-Anchor-Alignment-light-css-anchor-positioning-linux.png index 4d42f44e486..a1d1eb327c3 100644 Binary files a/.playwright/snapshots/components/AnchoredOverlay.test.ts-snapshots/AnchoredOverlay-Anchor-Alignment-light-css-anchor-positioning-linux.png and b/.playwright/snapshots/components/AnchoredOverlay.test.ts-snapshots/AnchoredOverlay-Anchor-Alignment-light-css-anchor-positioning-linux.png differ diff --git a/.playwright/snapshots/components/AnchoredOverlay.test.ts-snapshots/AnchoredOverlay-Anchor-Position-Grid-light-css-anchor-positioning-linux.png b/.playwright/snapshots/components/AnchoredOverlay.test.ts-snapshots/AnchoredOverlay-Anchor-Position-Grid-light-css-anchor-positioning-linux.png index cfc272e058d..7ca823934ce 100644 Binary files a/.playwright/snapshots/components/AnchoredOverlay.test.ts-snapshots/AnchoredOverlay-Anchor-Position-Grid-light-css-anchor-positioning-linux.png and b/.playwright/snapshots/components/AnchoredOverlay.test.ts-snapshots/AnchoredOverlay-Anchor-Position-Grid-light-css-anchor-positioning-linux.png differ diff --git a/.playwright/snapshots/components/AnchoredOverlay.test.ts-snapshots/AnchoredOverlay-Fullscreen-Variant-light-css-anchor-positioning-linux.png b/.playwright/snapshots/components/AnchoredOverlay.test.ts-snapshots/AnchoredOverlay-Fullscreen-Variant-light-css-anchor-positioning-linux.png index edb90f94d08..d8905c4e284 100644 Binary files a/.playwright/snapshots/components/AnchoredOverlay.test.ts-snapshots/AnchoredOverlay-Fullscreen-Variant-light-css-anchor-positioning-linux.png and b/.playwright/snapshots/components/AnchoredOverlay.test.ts-snapshots/AnchoredOverlay-Fullscreen-Variant-light-css-anchor-positioning-linux.png differ diff --git a/.playwright/snapshots/components/AnchoredOverlay.test.ts-snapshots/AnchoredOverlay-Offset-Alignment-From-Anchor-light-css-anchor-positioning-linux.png b/.playwright/snapshots/components/AnchoredOverlay.test.ts-snapshots/AnchoredOverlay-Offset-Alignment-From-Anchor-light-css-anchor-positioning-linux.png index 0dfe120c911..9fd40da8ea8 100644 Binary files a/.playwright/snapshots/components/AnchoredOverlay.test.ts-snapshots/AnchoredOverlay-Offset-Alignment-From-Anchor-light-css-anchor-positioning-linux.png and b/.playwright/snapshots/components/AnchoredOverlay.test.ts-snapshots/AnchoredOverlay-Offset-Alignment-From-Anchor-light-css-anchor-positioning-linux.png differ diff --git a/.playwright/snapshots/components/AnchoredOverlay.test.ts-snapshots/AnchoredOverlay-Offset-Position-From-Anchor-light-css-anchor-positioning-linux.png b/.playwright/snapshots/components/AnchoredOverlay.test.ts-snapshots/AnchoredOverlay-Offset-Position-From-Anchor-light-css-anchor-positioning-linux.png index b3853ddc166..cd1446b4fb2 100644 Binary files a/.playwright/snapshots/components/AnchoredOverlay.test.ts-snapshots/AnchoredOverlay-Offset-Position-From-Anchor-light-css-anchor-positioning-linux.png and b/.playwright/snapshots/components/AnchoredOverlay.test.ts-snapshots/AnchoredOverlay-Offset-Position-From-Anchor-light-css-anchor-positioning-linux.png differ diff --git a/.playwright/snapshots/components/AnchoredOverlay.test.ts-snapshots/AnchoredOverlay-Scroll-With-Anchor-light-css-anchor-positioning-linux.png b/.playwright/snapshots/components/AnchoredOverlay.test.ts-snapshots/AnchoredOverlay-Scroll-With-Anchor-light-css-anchor-positioning-linux.png index 940be94e376..6b80e1f6afe 100644 Binary files a/.playwright/snapshots/components/AnchoredOverlay.test.ts-snapshots/AnchoredOverlay-Scroll-With-Anchor-light-css-anchor-positioning-linux.png and b/.playwright/snapshots/components/AnchoredOverlay.test.ts-snapshots/AnchoredOverlay-Scroll-With-Anchor-light-css-anchor-positioning-linux.png differ diff --git a/package-lock.json b/package-lock.json index e4d9cbafc77..f31d6e38150 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4816,21 +4816,23 @@ } }, "node_modules/@floating-ui/core": { - "version": "1.7.4", - "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.4.tgz", - "integrity": "sha512-C3HlIdsBxszvm5McXlB8PeOEWfBhcGBTZGkGlWc2U0KFY5IwG5OQEuQ8rq52DZmcHDlPLd+YFBK+cZcytwIFWg==", + "version": "1.7.3", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.3.tgz", + "integrity": "sha512-sGnvb5dmrJaKEZ+LDIpguvdX3bDlEllmv4/ClQ9awcmCZrlx5jQyyMWFM5kBI+EyNOCDDiKk8il0zeuX3Zlg/w==", + "dev": true, "license": "MIT", "dependencies": { "@floating-ui/utils": "^0.2.10" } }, "node_modules/@floating-ui/dom": { - "version": "1.7.5", - "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.5.tgz", - "integrity": "sha512-N0bD2kIPInNHUHehXhMke1rBGs1dwqvC9O9KYMyyjK7iXt7GAhnro7UlcuYcGdS/yYOlq0MAVgrow8IbWJwyqg==", + "version": "1.7.4", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.4.tgz", + "integrity": "sha512-OOchDgh4F2CchOX94cRVqhvy7b3AFb+/rQXyswmzmGakRfkMgoWVjfnLWkRirfLEfuD4ysVW16eXzwt3jHIzKA==", + "dev": true, "license": "MIT", "dependencies": { - "@floating-ui/core": "^1.7.4", + "@floating-ui/core": "^1.7.3", "@floating-ui/utils": "^0.2.10" } }, @@ -4852,6 +4854,7 @@ "version": "0.2.10", "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.10.tgz", "integrity": "sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==", + "dev": true, "license": "MIT" }, "node_modules/@github-ui/storybook-addon-performance-panel": { @@ -6474,55 +6477,6 @@ "node": "^16.13.0 || >=18.0.0" } }, - "node_modules/@oddbird/css-anchor-positioning": { - "version": "0.9.0", - "resolved": "https://registry.npmjs.org/@oddbird/css-anchor-positioning/-/css-anchor-positioning-0.9.0.tgz", - "integrity": "sha512-G5nfb4sU0auxJH7VHafPwVJjr1GhH5uPSkmytGqhNftCpT3QEh8pFtMd4YHt1dRwb4o9qVZxlGSKUIc4TIrysQ==", - "license": "BSD-3-Clause", - "dependencies": { - "@floating-ui/dom": "^1.7.5", - "@types/css-tree": "^2.3.11", - "css-tree": "^3.1.0", - "nanoid": "^5.1.6" - } - }, - "node_modules/@oddbird/css-anchor-positioning/node_modules/css-tree": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-3.1.0.tgz", - "integrity": "sha512-0eW44TGN5SQXU1mWSkKwFstI/22X2bG1nYzZTYMAWjylYURhse752YgbE4Cx46AC+bAvI+/dYTPRk1LqSUnu6w==", - "license": "MIT", - "dependencies": { - "mdn-data": "2.12.2", - "source-map-js": "^1.0.1" - }, - "engines": { - "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0" - } - }, - "node_modules/@oddbird/css-anchor-positioning/node_modules/mdn-data": { - "version": "2.12.2", - "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.12.2.tgz", - "integrity": "sha512-IEn+pegP1aManZuckezWCO+XZQDplx1366JoVhTpMpBB1sPey/SbveZQUosKiKiGYjg1wH4pMlNgXbCiYgihQA==", - "license": "CC0-1.0" - }, - "node_modules/@oddbird/css-anchor-positioning/node_modules/nanoid": { - "version": "5.1.6", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-5.1.6.tgz", - "integrity": "sha512-c7+7RQ+dMB5dPwwCp4ee1/iV/q2P6aK1mTZcfr1BTuVlyW9hJYiMPybJCcnBlQtuSmTIWNeazm/zqNoZSSElBg==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "bin": { - "nanoid": "bin/nanoid.js" - }, - "engines": { - "node": "^18 || >=20" - } - }, "node_modules/@pkgjs/parseargs": { "version": "0.11.0", "dev": true, @@ -9083,12 +9037,6 @@ "@types/node": "*" } }, - "node_modules/@types/css-tree": { - "version": "2.3.11", - "resolved": "https://registry.npmjs.org/@types/css-tree/-/css-tree-2.3.11.tgz", - "integrity": "sha512-aEokibJOI77uIlqoBOkVbaQGC9zII0A+JH1kcTNKW2CwyYWD8KM6qdo+4c77wD3wZOQfJuNWAr9M4hdk+YhDIg==", - "license": "MIT" - }, "node_modules/@types/debug": { "version": "4.1.12", "dev": true, @@ -27795,7 +27743,6 @@ "@github/relative-time-element": "^4.5.0", "@github/tab-container-element": "^4.8.2", "@lit-labs/react": "1.2.1", - "@oddbird/css-anchor-positioning": "^0.9.0", "@oddbird/popover-polyfill": "^0.5.2", "@primer/behaviors": "^1.10.2", "@primer/live-region-element": "^0.7.1", diff --git a/packages/react/package.json b/packages/react/package.json index ce4b65c4361..44dc3e7f329 100644 --- a/packages/react/package.json +++ b/packages/react/package.json @@ -78,7 +78,6 @@ "@github/relative-time-element": "^4.5.0", "@github/tab-container-element": "^4.8.2", "@lit-labs/react": "1.2.1", - "@oddbird/css-anchor-positioning": "^0.9.0", "@oddbird/popover-polyfill": "^0.5.2", "@primer/behaviors": "^1.10.2", "@primer/live-region-element": "^0.7.1", diff --git a/packages/react/src/ActionMenu/ActionMenu.examples.stories.tsx b/packages/react/src/ActionMenu/ActionMenu.examples.stories.tsx index d55efb4b151..1f2b3bf420e 100644 --- a/packages/react/src/ActionMenu/ActionMenu.examples.stories.tsx +++ b/packages/react/src/ActionMenu/ActionMenu.examples.stories.tsx @@ -759,95 +759,3 @@ export const InsideDialog = () => { ) } - -export const CenteredOnPage = () => { - const [open, setOpen] = React.useState(false) - - return ( -
- - Open menu - - - alert('Copy link clicked')}> - Copy link - ⌘C - - alert('Quote reply clicked')}> - Quote reply - ⌘Q - - alert('Edit comment clicked')}> - Edit comment - ⌘E - - - alert('Delete file clicked')}> - Delete file - ⌘D - - - - -
- ) -} - -export const TwoActionMenus = () => { - return ( -
- - First menu - - - alert('Copy clicked')}> - - - - Copy - ⌘C - - alert('Archive clicked')}> - - - - Archive - - - alert('Delete clicked')}> - Delete - ⌘D - - - - - - - Second menu - - - alert('Settings clicked')}> - - - - Settings - - alert('Workflows clicked')}> - - - - Workflows - - - - - - - Documentation - - - - -
- ) -} diff --git a/packages/react/src/ActionMenu/ActionMenu.test.tsx b/packages/react/src/ActionMenu/ActionMenu.test.tsx index 8d00bb8ea28..f7e74c3db77 100644 --- a/packages/react/src/ActionMenu/ActionMenu.test.tsx +++ b/packages/react/src/ActionMenu/ActionMenu.test.tsx @@ -9,7 +9,6 @@ import {Tooltip as TooltipV2} from '../TooltipV2/Tooltip' import {SingleSelect} from '../ActionMenu/ActionMenu.features.stories' import {MixedSelection} from '../ActionMenu/ActionMenu.examples.stories' import {SearchIcon, KebabHorizontalIcon} from '@primer/octicons-react' -import anchoredOverlayClasses from '../AnchoredOverlay/AnchoredOverlay.module.css' import {getAnchoredPosition} from '@primer/behaviors' import type {AnchorPosition} from '@primer/behaviors' @@ -623,124 +622,58 @@ describe('ActionMenu', () => { expect(baseAnchor).not.toHaveAttribute('aria-expanded', 'true') }) - it('supports className prop on ActionMenu.Anchor with css anchor positioning flag', async () => { - const component = HTMLRender( - - - - - - - - - New file - - Copy link - Edit file - event.preventDefault()}> - Delete file - - - Github - - - - - - , - ) - const anchor = component.getByRole('button', {name: 'Toggle Menu'}) - expect(anchor).toHaveClass('test-class') - expect(anchor).toHaveClass(anchoredOverlayClasses.Anchor) - }) - - it('supports className prop on ActionMenu.Button with css anchor positioning flag', async () => { - const component = HTMLRender( - - - - Toggle Menu - - - New file - - Copy link - Edit file - event.preventDefault()}> - Delete file - - - Github - - - - - - , - ) - const button = component.getByRole('button', {name: 'Toggle Menu'}) - expect(button).toHaveClass('test-class') - expect(button).toHaveClass(anchoredOverlayClasses.Anchor) - }) - it('supports className prop on ActionMenu.Anchor', async () => { const component = HTMLRender( - - - - - - - - - New file - - Copy link - Edit file - event.preventDefault()}> - Delete file - - - Github - - - - - - , + + + + + + + + New file + + Copy link + Edit file + event.preventDefault()}> + Delete file + + + Github + + + + + , ) const anchor = component.getByRole('button', {name: 'Toggle Menu'}) expect(anchor).toHaveClass('test-class') - expect(anchor).not.toHaveClass(anchoredOverlayClasses.Anchor) }) it('supports className prop on ActionMenu.Button', async () => { const component = HTMLRender( - - - - Toggle Menu - - - New file - - Copy link - Edit file - event.preventDefault()}> - Delete file - - - Github - - - - - - , + + + Toggle Menu + + + New file + + Copy link + Edit file + event.preventDefault()}> + Delete file + + + Github + + + + + , ) const button = component.getByRole('button', {name: 'Toggle Menu'}) expect(button).toHaveClass('test-class') - expect(button).not.toHaveClass(anchoredOverlayClasses.Anchor) }) }) diff --git a/packages/react/src/ActionMenu/ActionMenu.tsx b/packages/react/src/ActionMenu/ActionMenu.tsx index d36ae802ca7..e501b117377 100644 --- a/packages/react/src/ActionMenu/ActionMenu.tsx +++ b/packages/react/src/ActionMenu/ActionMenu.tsx @@ -1,5 +1,4 @@ import React, {useCallback, useContext, useMemo, useEffect, useState} from 'react' -import {clsx} from 'clsx' import {TriangleDownIcon, ChevronRightIcon} from '@primer/octicons-react' import type {AnchoredOverlayProps} from '../AnchoredOverlay' import {AnchoredOverlay} from '../AnchoredOverlay' @@ -74,10 +73,6 @@ const mergeAnchorHandlers = (anchorProps: React.HTMLAttributes, but mergedAnchorProps.onKeyDown = mergedOnAnchorKeyDown } - if (buttonProps.className) { - mergedAnchorProps.className = clsx(anchorProps.className, buttonProps.className) - } - return mergedAnchorProps } @@ -158,11 +153,7 @@ const Menu: FCWithSlotMarker> = ({ } } } else { - renderAnchor = anchorProps => - React.cloneElement(child, { - ...anchorProps, - className: clsx(anchorProps.className, child.props.className), - }) + renderAnchor = anchorProps => React.cloneElement(child, anchorProps) } return null } else if (child.type === MenuButton || isSlot(child, MenuButton)) { @@ -243,7 +234,6 @@ const Anchor: WithSlotMarker< {React.cloneElement(child, { ...anchorProps, ref: anchorRef, - className: clsx(anchorProps.className, child.props.className), onClick: onButtonClick, onKeyDown: onButtonKeyDown, })} diff --git a/packages/react/src/AnchoredOverlay/AnchoredOverlay.module.css b/packages/react/src/AnchoredOverlay/AnchoredOverlay.module.css index 72485a8d665..512a890b5ff 100644 --- a/packages/react/src/AnchoredOverlay/AnchoredOverlay.module.css +++ b/packages/react/src/AnchoredOverlay/AnchoredOverlay.module.css @@ -12,48 +12,3 @@ display: inline-grid; } } - -.Wrapper { - anchor-scope: --anchored-overlay-anchor; -} - -.Anchor { - /* Anchor name, this is currently tied to `renderAnchor` */ - anchor-name: --anchored-overlay-anchor; -} - -.AnchoredOverlay { - /* Anchor position, this is currently tied to `` */ - position-anchor: --anchored-overlay-anchor; - position-try-fallbacks: - flip-block, - flip-inline, - flip-block flip-inline; - position-visibility: anchors-visible; - z-index: 100; - position: fixed; - - &[data-side='outside-bottom'] { - /* stylelint-disable primer/spacing */ - top: calc(anchor(bottom) + var(--base-size-4)); - left: anchor(left); - } - - &[data-side='outside-top'] { - margin-bottom: var(--base-size-4); - bottom: anchor(top); - left: anchor(left); - } - - &[data-side='outside-left'] { - right: anchor(left); - top: anchor(top); - margin-right: var(--base-size-4); - } - - &[data-side='outside-right'] { - left: anchor(right); - top: anchor(top); - margin-left: var(--base-size-4); - } -} diff --git a/packages/react/src/AnchoredOverlay/AnchoredOverlay.test.tsx b/packages/react/src/AnchoredOverlay/AnchoredOverlay.test.tsx index 8bcb8d46284..279bcc112d1 100644 --- a/packages/react/src/AnchoredOverlay/AnchoredOverlay.test.tsx +++ b/packages/react/src/AnchoredOverlay/AnchoredOverlay.test.tsx @@ -7,10 +7,8 @@ import {Button} from '../Button' import BaseStyles from '../BaseStyles' import type {AnchorPosition} from '@primer/behaviors' import {implementsClassName} from '../utils/testing' -import {FeatureFlags} from '../FeatureFlags' import overlayClasses from '../Overlay/Overlay.module.css' -import anchoredOverlayClasses from './AnchoredOverlay.module.css' type TestComponentSettings = { initiallyOpen?: boolean @@ -18,7 +16,6 @@ type TestComponentSettings = { onCloseCallback?: (gesture: string) => void onPositionChange?: ({position}: {position: AnchorPosition}) => void className?: string - withCSSAnchorPositioningFeatureFlag?: boolean } const AnchoredOverlayTestComponent = ({ @@ -27,7 +24,6 @@ const AnchoredOverlayTestComponent = ({ onCloseCallback, onPositionChange, className, - withCSSAnchorPositioningFeatureFlag, }: TestComponentSettings = {}) => { const [open, setOpen] = useState(initiallyOpen) const onOpen = useCallback( @@ -44,8 +40,7 @@ const AnchoredOverlayTestComponent = ({ }, [onCloseCallback], ) - - const content = ( + return ( ) - - if (withCSSAnchorPositioningFeatureFlag !== undefined) { - return ( - - {content} - - ) - } - - return content } -describe.each([true, false])( - 'AnchoredOverlay (primer_react_css_anchor_positioning=%s)', - (withCSSAnchorPositioningFeatureFlag: boolean) => { - implementsClassName( - props => ( - - ), - overlayClasses.Overlay, +describe('AnchoredOverlay', () => { + implementsClassName(props => , overlayClasses.Overlay) + it('should call onOpen when the anchor is clicked', async () => { + const mockOpenCallback = vi.fn() + const mockCloseCallback = vi.fn() + const anchoredOverlay = render( + , ) - - it('should call onOpen when the anchor is clicked', async () => { - const mockOpenCallback = vi.fn() - const mockCloseCallback = vi.fn() - const anchoredOverlay = render( - , - ) - const anchor = anchoredOverlay.baseElement.querySelector('[aria-haspopup="true"]')! - await act(async () => { - await userEvent.click(anchor) - }) - - expect(mockOpenCallback).toHaveBeenCalledTimes(1) - expect(mockOpenCallback).toHaveBeenCalledWith('anchor-click') - expect(mockCloseCallback).toHaveBeenCalledTimes(0) - }) - - it('should call onOpen when the anchor activated by a key press', async () => { - const mockOpenCallback = vi.fn() - const mockCloseCallback = vi.fn() - const anchoredOverlay = render( - , - ) - const anchor = anchoredOverlay.baseElement.querySelector('[aria-haspopup="true"]')! - await act(async () => { - await userEvent.type(anchor, '{Space}') - }) - - expect(mockOpenCallback).toHaveBeenCalledTimes(1) - expect(mockOpenCallback).toHaveBeenCalledWith('anchor-key-press') - expect(mockCloseCallback).toHaveBeenCalledTimes(0) - }) - - it('should call onClose when the user clicks off of the overlay', async () => { - const mockOpenCallback = vi.fn() - const mockCloseCallback = vi.fn() - const anchoredOverlay = render( - , - ) - await act(async () => { - await userEvent.click(anchoredOverlay.baseElement) - }) - - expect(mockOpenCallback).toHaveBeenCalledTimes(0) - expect(mockCloseCallback).toHaveBeenCalledTimes(1) - expect(mockCloseCallback).toHaveBeenCalledWith('click-outside') - }) - - it('should call onClose when the escape key is pressed', async () => { - const mockOpenCallback = vi.fn() - const mockCloseCallback = vi.fn() - - render( - , - ) - - await act(async () => { - await userEvent.keyboard('{Escape}') - }) - - expect(mockOpenCallback).toHaveBeenCalledTimes(0) - expect(mockCloseCallback).toHaveBeenCalledTimes(1) - expect(mockCloseCallback).toHaveBeenCalledWith('escape') - }) - - it('should call onPositionChange when provided', async () => { - const mockPositionChangeCallback = vi.fn(({position}: {position: AnchorPosition}) => position) - render( - , - ) - - await act(async () => { - await userEvent.keyboard('{Escape}') - }) - - expect(mockPositionChangeCallback).toHaveBeenCalled() - expect(mockPositionChangeCallback).toHaveBeenCalledWith({ - position: { - anchorAlign: 'start', - anchorSide: 'outside-bottom', - left: 0, - top: 36, - }, - }) - }) - - it('should support a `ref` through `overlayProps` on the overlay element', () => { - const ref = createRef() - - function Test() { - const anchorRef = useRef(null) - return ( - - { - return ( - - ) - }} - > -
content
-
-
- ) - } - - render() - - expect(document.getElementById('overlay')).toBe(ref.current) - }) - }, -) - -describe('AnchoredOverlay feature flag specific behavior', () => { - describe('with primer_react_css_anchor_positioning feature flag enabled', () => { - it('should render wrapper div when flag is enabled', () => { - const {container} = render( - - - , - ) - - const wrapper = container.querySelector(`.${anchoredOverlayClasses.Wrapper}`) - expect(wrapper).toBeInTheDocument() + const anchor = anchoredOverlay.baseElement.querySelector('[aria-haspopup="true"]')! + await act(async () => { + await userEvent.click(anchor) }) - it('should apply Anchor class to anchor element when flag is enabled', () => { - const {container} = render( - - - , - ) + expect(mockOpenCallback).toHaveBeenCalledTimes(1) + expect(mockOpenCallback).toHaveBeenCalledWith('anchor-click') + expect(mockCloseCallback).toHaveBeenCalledTimes(0) + }) - const anchor = container.querySelector('[aria-haspopup="true"]') - expect(anchor).toHaveClass(anchoredOverlayClasses.Anchor) + it('should call onOpen when the anchor activated by a key press', async () => { + const mockOpenCallback = vi.fn() + const mockCloseCallback = vi.fn() + const anchoredOverlay = render( + , + ) + const anchor = anchoredOverlay.baseElement.querySelector('[aria-haspopup="true"]')! + await act(async () => { + await userEvent.type(anchor, '{Space}') }) - it('should render overlay as visible immediately when flag is enabled', () => { - const {baseElement} = render( - - - , - ) + expect(mockOpenCallback).toHaveBeenCalledTimes(1) + expect(mockOpenCallback).toHaveBeenCalledWith('anchor-key-press') + expect(mockCloseCallback).toHaveBeenCalledTimes(0) + }) - const overlay = baseElement.querySelector('[data-component="AnchoredOverlay"]') - expect(overlay).toHaveAttribute('data-visibility-visible', '') + it('should call onClose when the user clicks off of the overlay', async () => { + const mockOpenCallback = vi.fn() + const mockCloseCallback = vi.fn() + const anchoredOverlay = render( + , + ) + await act(async () => { + await userEvent.click(anchoredOverlay.baseElement) }) - it('should not use portal when flag is enabled', () => { - const {baseElement, container} = render( - - - , - ) + expect(mockOpenCallback).toHaveBeenCalledTimes(0) + expect(mockCloseCallback).toHaveBeenCalledTimes(1) + expect(mockCloseCallback).toHaveBeenCalledWith('click-outside') + }) - // The overlay should be inside the component tree, not in the portal root - const portalRoot = baseElement.querySelector('#__primerPortalRoot__') - const overlayInPortal = portalRoot?.querySelector('[data-component="AnchoredOverlay"]') - expect(overlayInPortal).toBeNull() + it('should call onClose when the escape key is pressed', async () => { + const mockOpenCallback = vi.fn() + const mockCloseCallback = vi.fn() - // The overlay should be inside the wrapper - const wrapper = container.querySelector(`.${anchoredOverlayClasses.Wrapper}`) - const overlayInWrapper = wrapper?.querySelector('[data-component="AnchoredOverlay"]') - expect(overlayInWrapper).toBeInTheDocument() - }) - - it('should apply AnchoredOverlay class to overlay when flag is enabled', () => { - const {baseElement} = render( - - - , - ) + render( + , + ) - const overlay = baseElement.querySelector('[data-component="AnchoredOverlay"]') - expect(overlay).toHaveClass(anchoredOverlayClasses.AnchoredOverlay) + await act(async () => { + await userEvent.keyboard('{Escape}') }) - it('should set data-anchor-position attribute when flag is enabled', () => { - const {baseElement} = render( - - - , - ) - - const overlay = baseElement.querySelector('[data-component="AnchoredOverlay"]') - expect(overlay).toHaveAttribute('data-anchor-position', 'true') - }) + expect(mockOpenCallback).toHaveBeenCalledTimes(0) + expect(mockCloseCallback).toHaveBeenCalledTimes(1) + expect(mockCloseCallback).toHaveBeenCalledWith('escape') }) - describe('with primer_react_css_anchor_positioning feature flag disabled', () => { - it('should not render wrapper div when flag is disabled', () => { - const {container} = render( - - - , - ) + it('should call onPositionChange when provided', async () => { + const mockPositionChangeCallback = vi.fn(({position}: {position: AnchorPosition}) => position) + render() - const wrapper = container.querySelector(`.${anchoredOverlayClasses.Wrapper}`) - expect(wrapper).not.toBeInTheDocument() + await act(async () => { + await userEvent.keyboard('{Escape}') }) - it('should not apply Anchor class to anchor element when flag is disabled', () => { - const {container} = render( - - - , - ) - - const anchor = container.querySelector('[aria-haspopup="true"]') - expect(anchor).not.toHaveClass(anchoredOverlayClasses.Anchor) + expect(mockPositionChangeCallback).toHaveBeenCalled() + expect(mockPositionChangeCallback).toHaveBeenCalledWith({ + position: { + anchorAlign: 'start', + anchorSide: 'outside-bottom', + left: 0, + top: 36, + }, }) + }) - it('should use portal when flag is disabled', () => { - const {baseElement} = render( - - - , + it('should support a `ref` through `overlayProps` on the overlay element', () => { + const ref = createRef() + + function Test() { + const anchorRef = useRef(null) + return ( + { + return ( + + ) + }} + > +
content
+
) + } - // The overlay should be inside the portal root - const portalRoot = baseElement.querySelector('#__primerPortalRoot__') - const overlayInPortal = portalRoot?.querySelector('[data-component="AnchoredOverlay"]') - expect(overlayInPortal).toBeInTheDocument() - }) + render() - it('should set data-anchor-position to false when flag is disabled', () => { - const {baseElement} = render( - - - , - ) - - const overlay = baseElement.querySelector('[data-component="AnchoredOverlay"]') - expect(overlay).toHaveAttribute('data-anchor-position', 'false') - }) + expect(document.getElementById('overlay')).toBe(ref.current) }) }) diff --git a/packages/react/src/AnchoredOverlay/AnchoredOverlay.tsx b/packages/react/src/AnchoredOverlay/AnchoredOverlay.tsx index 3f9513f94f9..f60070447b3 100644 --- a/packages/react/src/AnchoredOverlay/AnchoredOverlay.tsx +++ b/packages/react/src/AnchoredOverlay/AnchoredOverlay.tsx @@ -14,7 +14,6 @@ import {IconButton, type IconButtonProps} from '../Button' import {XIcon} from '@primer/octicons-react' import classes from './AnchoredOverlay.module.css' import {clsx} from 'clsx' -import {useFeatureFlag} from '../FeatureFlags' interface AnchoredOverlayPropsWithAnchor { /** @@ -124,13 +123,6 @@ export type AnchoredOverlayProps = AnchoredOverlayBaseProps & (AnchoredOverlayPropsWithAnchor | AnchoredOverlayPropsWithoutAnchor) & Partial> -const applyAnchorPositioningPolyfill = async () => { - if (typeof window !== 'undefined' && !('anchorName' in document.documentElement.style)) { - const {default: polyfill} = await import('@oddbird/css-anchor-positioning/fn') - polyfill() - } -} - const defaultVariant = { regular: 'anchored', narrow: 'anchored', @@ -168,7 +160,6 @@ export const AnchoredOverlay: React.FC { - const cssAnchorPositioning = useFeatureFlag('primer_react_css_anchor_positioning') const anchorRef = useProvidedRefOrCreate(externalAnchorRef) const [overlayRef, updateOverlayRef] = useRenderForcingRef() const anchorId = useId(externalAnchorId) @@ -227,11 +218,7 @@ export const AnchoredOverlay: React.FC {renderAnchor && renderAnchor({ @@ -257,7 +242,6 @@ export const AnchoredOverlay: React.FC { if (overlayProps?.ref) { assignRef(overlayProps.ref, node) } updateOverlayRef(node) }} - data-anchor-position={cssAnchorPositioning} - data-side={cssAnchorPositioning ? side : position?.anchorSide} > {showXIcon ? (
@@ -309,12 +291,6 @@ export const AnchoredOverlay: React.FC ) - - if (cssAnchorPositioning) { - return
{innerContent}
- } - - return innerContent } function assignRef( diff --git a/packages/react/src/FeatureFlags/DefaultFeatureFlags.ts b/packages/react/src/FeatureFlags/DefaultFeatureFlags.ts index e3d5fd0d975..796448887e8 100644 --- a/packages/react/src/FeatureFlags/DefaultFeatureFlags.ts +++ b/packages/react/src/FeatureFlags/DefaultFeatureFlags.ts @@ -2,7 +2,6 @@ import {FeatureFlagScope} from './FeatureFlagScope' export const DefaultFeatureFlags = FeatureFlagScope.create({ primer_react_breadcrumbs_overflow_menu: false, - primer_react_css_anchor_positioning: false, primer_react_css_has_selector_perf: false, primer_react_select_panel_fullscreen_on_narrow: false, primer_react_select_panel_order_selected_at_top: false, diff --git a/packages/react/src/Overlay/Overlay.module.css b/packages/react/src/Overlay/Overlay.module.css index 232058eb107..497c5a412f1 100644 --- a/packages/react/src/Overlay/Overlay.module.css +++ b/packages/react/src/Overlay/Overlay.module.css @@ -19,29 +19,14 @@ background-color: var(--overlay-bgColor); border-radius: var(--borderRadius-large); box-shadow: var(--shadow-floating-small); - - &[data-anchor-position='false'], - &:not([data-anchor-position]):not([data-variant='modal']) { - /* stylelint-disable-next-line primer/spacing */ - right: var(--right, auto); - /* stylelint-disable-next-line primer/spacing */ - bottom: var(--bottom, auto); - - /* stylelint-disable-next-line selector-max-specificity */ - &[data-responsive='fullscreen']:not([data-variant]) { - @media screen and (--viewportRange-narrow) { - top: 0; - left: 0; - } - } - - &:not([data-variant]) { - /* stylelint-disable-next-line primer/spacing */ - top: var(--top, auto); - /* stylelint-disable-next-line primer/spacing */ - left: var(--left, auto); - } - } + /* stylelint-disable-next-line primer/spacing */ + top: var(--top, auto); + /* stylelint-disable-next-line primer/spacing */ + left: var(--left, auto); + /* stylelint-disable-next-line primer/spacing */ + right: var(--right, auto); + /* stylelint-disable-next-line primer/spacing */ + bottom: var(--bottom, auto); &:focus { outline: none; diff --git a/packages/react/src/Overlay/Overlay.tsx b/packages/react/src/Overlay/Overlay.tsx index 6db7e9debac..362083945e2 100644 --- a/packages/react/src/Overlay/Overlay.tsx +++ b/packages/react/src/Overlay/Overlay.tsx @@ -10,7 +10,6 @@ import type {AnchorSide} from '@primer/behaviors' import type {ForwardRefComponent as PolymorphicForwardRefComponent} from '../utils/polymorphic' import classes from './Overlay.module.css' import {clsx} from 'clsx' -import {useFeatureFlag} from '../FeatureFlags' type StyledOverlayProps = { width?: keyof typeof widthMap @@ -190,7 +189,6 @@ const Overlay = React.forwardRef( forwardedRef, // eslint-disable-next-line @typescript-eslint/no-explicit-any ): ReactElement => { - const cssAnchorPositioning = useFeatureFlag('primer_react_css_anchor_positioning') const overlayRef = useRef(null) useRefObjectAsForwardedRef(forwardedRef, overlayRef) const slideAnimationDistance = 8 // var(--base-size-8), hardcoded to do some math @@ -231,26 +229,22 @@ const Overlay = React.forwardRef( // To be backwards compatible with the old Overlay, we need to set the left prop if x-position is not specified const leftPosition = left === undefined && right === undefined ? 0 : left - const overlayContent = ( - + return ( + + + ) - - if (cssAnchorPositioning) { - return overlayContent - } - - return {overlayContent} }, ) as PolymorphicForwardRefComponent<'div', internalOverlayProps>