From c269372d26cb9f1cb822bb2ffd6bff5dc09a02dd Mon Sep 17 00:00:00 2001 From: Marie Lucca Date: Mon, 18 Nov 2024 16:10:37 -0500 Subject: [PATCH] feat(Overlay): Convert Overlay to CSS modules behind team feature flag --- e2e/components/Overlay.test.ts | 134 +++++++++++++++ .../react/src/Overlay/Overlay.dev.stories.tsx | 81 +++++++++ packages/react/src/Overlay/Overlay.module.css | 158 +++++++++++++++++ packages/react/src/Overlay/Overlay.tsx | 162 ++++++++++++------ .../experimental/SelectPanel2/SelectPanel.tsx | 6 +- 5 files changed, 481 insertions(+), 60 deletions(-) create mode 100644 e2e/components/Overlay.test.ts create mode 100644 packages/react/src/Overlay/Overlay.dev.stories.tsx create mode 100644 packages/react/src/Overlay/Overlay.module.css diff --git a/e2e/components/Overlay.test.ts b/e2e/components/Overlay.test.ts new file mode 100644 index 00000000000..e9a826c3d6c --- /dev/null +++ b/e2e/components/Overlay.test.ts @@ -0,0 +1,134 @@ +import {test, expect, type Page} from '@playwright/test' +import {visit} from '../test-helpers/storybook' +import {themes} from '../test-helpers/themes' + +const stories = [ + { + title: 'Default', + id: 'private-components-overlay--default', + setup: async (page: Page) => { + await page.keyboard.press('Tab') + await page.keyboard.press('Enter') + }, + }, + { + title: 'Playground', + id: 'private-components-overlay-dev--sx-props', + setup: async (page: Page) => { + await page.keyboard.press('Tab') + await page.keyboard.press('Enter') + }, + }, + { + title: 'Dialog Overlay', + id: 'private-components-overlay-features--dialog-overlay', + setup: async (page: Page) => { + await page.keyboard.press('Tab') + await page.keyboard.press('Enter') + }, + }, + { + title: 'Dropdown Overlay', + id: 'private-components-overlay-features--dropdown-overlay', + setup: async (page: Page) => { + await page.keyboard.press('Tab') + await page.keyboard.press('Enter') + }, + }, + { + title: 'Memex Issue Overlay', + id: 'private-components-overlay-features--memex-issue-overlay', + setup: async (page: Page) => { + await page.keyboard.press('Tab') + await page.keyboard.press('Enter') + }, + }, + { + title: 'Memex Nested Overlays', + id: 'private-components-overlay-features--memex-nested-overlays', + setup: async (page: Page) => { + await page.keyboard.press('Tab') + await page.keyboard.press('Tab') + await page.keyboard.press('Enter') + await page.keyboard.press('Tab') + await page.keyboard.press('Enter') + }, + }, + { + title: 'Nested Overlays', + id: 'private-components-overlay-features--nested-overlays', + setup: async (page: Page) => { + await page.keyboard.press('Tab') + await page.keyboard.press('Tab') + await page.keyboard.press('Tab') + await page.keyboard.press('Enter') + await page.keyboard.press('Tab') + await page.keyboard.press('Tab') + await page.keyboard.press('Enter') + }, + }, + { + title: 'Overlay On Top Of Overlay', + id: 'private-components-overlay-features--overlay-on-top-of-overlay', + setup: async (page: Page) => { + await page.keyboard.press('Tab') + await page.keyboard.press('Tab') + await page.keyboard.press('Enter') + await page.keyboard.press('Enter') + await page.keyboard.press('Enter') + }, + }, + { + title: 'Positioned Overlays', + id: 'private-components-overlay-features--positioned-overlays', + setup: async (page: Page) => { + await page.keyboard.press('Tab') + await page.keyboard.press('Tab') + await page.keyboard.press('Enter') + }, + }, + { + title: 'SX Props', + id: 'components-popover-dev--sx-props', + setup: async (page: Page) => { + await page.keyboard.press('Tab') + await page.keyboard.press('Enter') + }, + }, +] as const + +test.describe('Popover', () => { + for (const story of stories) { + test.describe(story.title, () => { + for (const theme of themes) { + test.describe(theme, () => { + test('@vrt', async ({page}) => { + await visit(page, { + id: story.id, + globals: { + colorScheme: theme, + }, + }) + + await story.setup(page) + + // Default state + expect(await page.screenshot()).toMatchSnapshot(`Overlay.${story.title}.${theme}.png`) + }) + + test('axe @aat', async ({page}) => { + await visit(page, { + id: story.id, + globals: { + colorScheme: theme, + }, + }) + await story.setup(page) + + await expect(page).toHaveNoViolations() + }) + }) + } + }) + } +}) diff --git a/packages/react/src/Overlay/Overlay.dev.stories.tsx b/packages/react/src/Overlay/Overlay.dev.stories.tsx new file mode 100644 index 00000000000..61c9c918c71 --- /dev/null +++ b/packages/react/src/Overlay/Overlay.dev.stories.tsx @@ -0,0 +1,81 @@ +import React, {useRef, useState} from 'react' +import type {Meta} from '@storybook/react' +import Text from '../Text' +import {Button, IconButton} from '../Button' +import Overlay from './Overlay' +import {useFocusTrap} from '../hooks/useFocusTrap' +import Box from '../Box' +import {XIcon} from '@primer/octicons-react' + +export default { + title: 'Private/Components/Overlay/Dev', + component: Overlay, +} as Meta + +export const SxProps = () => { + const [isOpen, setIsOpen] = useState(false) + const buttonRef = useRef(null) + const confirmButtonRef = useRef(null) + const anchorRef = useRef(null) + const closeOverlay = () => setIsOpen(false) + const containerRef = useRef(null) + useFocusTrap({ + containerRef, + disabled: !isOpen, + }) + return ( + + + {isOpen ? ( + + + + Look! an overlay + + + ) : null} + + ) +} diff --git a/packages/react/src/Overlay/Overlay.module.css b/packages/react/src/Overlay/Overlay.module.css new file mode 100644 index 00000000000..21e7de01ec8 --- /dev/null +++ b/packages/react/src/Overlay/Overlay.module.css @@ -0,0 +1,158 @@ +.Overlay { + position: absolute; + width: auto; + min-width: 192px; + height: auto; + overflow: hidden; + background-color: var(--overlay-bgColor); + border-radius: var(--borderRadius-large); + box-shadow: var(--shadow-floating-small); + animation: overlay-appear 200ms cubic-bezier(0.33, 1, 0.68, 1); + + @keyframes overlay-appear { + 0% { + opacity: 0; + } + + 100% { + opacity: 1; + } + } + + :focus { + outline: none; + } + + @media (forced-colors: active) { + /* Support for Windows high contrast https://sarahmhigley.com/writing/whcm-quick-tips */ + outline: solid 1px transparent; + } + + &:where([data-reflow-container='true']) { + max-width: calc(100vw - 2rem); + } + + &:where([data-overflow-auto]) { + overflow: auto; + } + + &:where([data-overflow-hidden]) { + overflow: hidden; + } + + &:where([data-overflow-scroll]) { + overflow: scroll; + } + + &:where([data-overflow-visible]) { + overflow: visible; + } + + &:where([data-height-xsmall]) { + height: 192px; + } + + &:where([data-height-small]) { + height: 256px; + } + + &:where([data-height-medium]) { + height: 320px; + } + + &:where([data-height-large]) { + height: 432px; + } + + &:where([data-height-xlarge]) { + height: 600px; + } + + &:where([data-height-auto]), + &:where([data-height-initial]) { + height: auto; + } + + &:where([data-height-fit-content]) { + height: fit-content; + } + + &:where([data-max-height-xsmall]) { + max-height: 192px; + } + + &:where([data-max-height-small]) { + max-height: 256px; + } + + &:where([data-max-height-medium]) { + max-height: 320px; + } + + &:where([data-max-height-large]) { + max-height: 432px; + } + + &:where([data-max-height-xlarge]) { + max-height: 600px; + } + + &:where([data-max-height-fit-content]) { + max-height: fit-content; + } + + &:where([data-width-small]) { + width: 256px; + } + + &:where([data-width-medium]) { + width: 320px; + } + + &:where([data-width-large]) { + /* stylelint-disable-next-line primer/responsive-widths */ + width: 480px; + } + + &:where([data-width-xlarge]) { + /* stylelint-disable-next-line primer/responsive-widths */ + width: 640px; + } + + &:where([data-width-xxlarge]) { + /* stylelint-disable-next-line primer/responsive-widths */ + width: 960px; + } + + &:where([data-width-auto]) { + width: auto; + } + + &:where([data-max-width-small]) { + max-width: 256px; + } + + &:where([data-max-width-medium]) { + max-width: 320px; + } + + &:where([data-max-width-large]) { + max-width: 480px; + } + + &:where([data-max-width-xlarge]) { + max-width: 640px; + } + + &:where([data-max-width-xxlarge]) { + max-width: 960px; + } + + &:where([data-visibility-visible]) { + visibility: visible; + } + + &:where([data-visibility-hidden]) { + visibility: hidden; + } +} \ No newline at end of file diff --git a/packages/react/src/Overlay/Overlay.tsx b/packages/react/src/Overlay/Overlay.tsx index ce69ef27f5c..306e6741c36 100644 --- a/packages/react/src/Overlay/Overlay.tsx +++ b/packages/react/src/Overlay/Overlay.tsx @@ -14,6 +14,11 @@ import type {AnchorSide} from '@primer/behaviors' import {useTheme} from '../ThemeProvider' import type {ForwardRefComponent as PolymorphicForwardRefComponent} from '../utils/polymorphic' import {useFeatureFlag} from '../FeatureFlags' +import {toggleStyledComponent} from '../internal/utils/toggleStyledComponent' +import classes from './Overlay.module.css' +import {clsx} from 'clsx' + +const CSS_MODULES_FLAG = 'primer_react_css_modules_team' type StyledOverlayProps = { width?: keyof typeof widthMap @@ -61,43 +66,48 @@ function getSlideAnimationStartingVector(anchorSide?: AnchorSide): {x: number; y return {x: 0, y: 0} } -export const StyledOverlay = styled.div` - background-color: ${get('colors.canvas.overlay')}; - box-shadow: ${get('shadows.overlay.shadow')}; - position: absolute; - min-width: 192px; - max-width: ${props => props.maxWidth && widthMap[props.maxWidth]}; - height: ${props => heightMap[props.height || 'auto']}; - max-height: ${props => props.maxHeight && heightMap[props.maxHeight]}; - width: ${props => widthMap[props.width || 'auto']}; - border-radius: 12px; - overflow: ${props => (props.overflow ? props.overflow : 'hidden')}; - animation: overlay-appear ${animationDuration}ms ${get('animation.easeOutCubic')}; - - @keyframes overlay-appear { - 0% { - opacity: 0; +export const BaseOverlay = toggleStyledComponent( + CSS_MODULES_FLAG, + 'div', + styled.div` + background-color: ${get('colors.canvas.overlay')}; + box-shadow: ${get('shadows.overlay.shadow')}; + position: absolute; + min-width: 192px; + max-width: ${props => props.maxWidth && widthMap[props.maxWidth]}; + height: ${props => heightMap[props.height || 'auto']}; + max-height: ${props => props.maxHeight && heightMap[props.maxHeight]}; + width: ${props => widthMap[props.width || 'auto']}; + border-radius: 12px; + overflow: ${props => (props.overflow ? props.overflow : 'hidden')}; + animation: overlay-appear ${animationDuration}ms ${get('animation.easeOutCubic')}; + + @keyframes overlay-appear { + 0% { + opacity: 0; + } + 100% { + opacity: 1; + } } - 100% { - opacity: 1; + visibility: var(--styled-overlay-visibility); + :focus { + outline: none; } - } - visibility: var(--styled-overlay-visibility); - :focus { - outline: none; - } - @media (forced-colors: active) { - /* Support for Windows high contrast https://sarahmhigley.com/writing/whcm-quick-tips */ - outline: solid 1px transparent; - } + @media (forced-colors: active) { + /* Support for Windows high contrast https://sarahmhigley.com/writing/whcm-quick-tips */ + outline: solid 1px transparent; + } - &[data-reflow-container='true'] { - max-width: calc(100vw - 2rem); - } + &[data-reflow-container='true'] { + max-width: calc(100vw - 2rem); + } + + ${sx}; + `, +) - ${sx}; -` type BaseOverlayProps = { ignoreClickRefs?: React.RefObject[] initialFocusRef?: React.RefObject @@ -116,6 +126,7 @@ type BaseOverlayProps = { role?: AriaRole children?: React.ReactNode preventOverflow?: boolean + className?: string } type OwnOverlayProps = Merge @@ -163,6 +174,9 @@ const Overlay = React.forwardRef( position, style: styleFromProps = {}, preventOverflow = true, + className, + maxHeight, + maxWidth, ...rest }, forwardedRef, @@ -208,31 +222,65 @@ 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: React.CSSProperties = left === undefined && right === undefined ? {left: 0} : {left} - const enabled = useFeatureFlag('primer_react_overlay_overflow') - - return ( - - - - ) + const overflowEnabled = useFeatureFlag('primer_react_overlay_overflow') + const cssModulesEnabled = useFeatureFlag(CSS_MODULES_FLAG) + + if (cssModulesEnabled) { + return ( + + + + ) + } else { + return ( + + + + ) + } }, ) as PolymorphicForwardRefComponent<'div', OwnOverlayProps> diff --git a/packages/react/src/experimental/SelectPanel2/SelectPanel.tsx b/packages/react/src/experimental/SelectPanel2/SelectPanel.tsx index ac53c6a4aac..d6a9846beda 100644 --- a/packages/react/src/experimental/SelectPanel2/SelectPanel.tsx +++ b/packages/react/src/experimental/SelectPanel2/SelectPanel.tsx @@ -19,7 +19,7 @@ import {ActionListContainerContext} from '../../ActionList/ActionListContainerCo import {useSlots} from '../../hooks/useSlots' import {useProvidedRefOrCreate, useId, useAnchoredPosition} from '../../hooks' import type {OverlayProps} from '../../Overlay/Overlay' -import {StyledOverlay, heightMap} from '../../Overlay/Overlay' +import {BaseOverlay, heightMap} from '../../Overlay/Overlay' import InputLabel from '../../internal/components/InputLabel' import {invariant} from '../../utils/invariant' import {AriaStatus} from '../../live-region' @@ -235,7 +235,7 @@ const Panel: React.FC = ({ <> {Anchor} - = ({ )} - + ) }