diff --git a/.changeset/three-falcons-compete.md b/.changeset/three-falcons-compete.md new file mode 100644 index 00000000000..be14c153e1f --- /dev/null +++ b/.changeset/three-falcons-compete.md @@ -0,0 +1,5 @@ +--- +"@primer/react": minor +--- + +Update `Popover` component to use CSS modules behind the feature flag primer_react_css_modules_team diff --git a/.playwright/snapshots/components/Popover.test.ts-snapshots/Popover-SX-Props-dark-colorblind-linux.png b/.playwright/snapshots/components/Popover.test.ts-snapshots/Popover-SX-Props-dark-colorblind-linux.png new file mode 100644 index 00000000000..2301e3f2048 Binary files /dev/null and b/.playwright/snapshots/components/Popover.test.ts-snapshots/Popover-SX-Props-dark-colorblind-linux.png differ diff --git a/.playwright/snapshots/components/Popover.test.ts-snapshots/Popover-SX-Props-dark-dimmed-linux.png b/.playwright/snapshots/components/Popover.test.ts-snapshots/Popover-SX-Props-dark-dimmed-linux.png new file mode 100644 index 00000000000..cff50cc2b22 Binary files /dev/null and b/.playwright/snapshots/components/Popover.test.ts-snapshots/Popover-SX-Props-dark-dimmed-linux.png differ diff --git a/.playwright/snapshots/components/Popover.test.ts-snapshots/Popover-SX-Props-dark-high-contrast-linux.png b/.playwright/snapshots/components/Popover.test.ts-snapshots/Popover-SX-Props-dark-high-contrast-linux.png new file mode 100644 index 00000000000..3bd4b88fbc4 Binary files /dev/null and b/.playwright/snapshots/components/Popover.test.ts-snapshots/Popover-SX-Props-dark-high-contrast-linux.png differ diff --git a/.playwright/snapshots/components/Popover.test.ts-snapshots/Popover-SX-Props-dark-linux.png b/.playwright/snapshots/components/Popover.test.ts-snapshots/Popover-SX-Props-dark-linux.png new file mode 100644 index 00000000000..5f048b5371e Binary files /dev/null and b/.playwright/snapshots/components/Popover.test.ts-snapshots/Popover-SX-Props-dark-linux.png differ diff --git a/.playwright/snapshots/components/Popover.test.ts-snapshots/Popover-SX-Props-dark-tritanopia-linux.png b/.playwright/snapshots/components/Popover.test.ts-snapshots/Popover-SX-Props-dark-tritanopia-linux.png new file mode 100644 index 00000000000..5f048b5371e Binary files /dev/null and b/.playwright/snapshots/components/Popover.test.ts-snapshots/Popover-SX-Props-dark-tritanopia-linux.png differ diff --git a/.playwright/snapshots/components/Popover.test.ts-snapshots/Popover-SX-Props-light-colorblind-linux.png b/.playwright/snapshots/components/Popover.test.ts-snapshots/Popover-SX-Props-light-colorblind-linux.png new file mode 100644 index 00000000000..ed864f28fb0 Binary files /dev/null and b/.playwright/snapshots/components/Popover.test.ts-snapshots/Popover-SX-Props-light-colorblind-linux.png differ diff --git a/.playwright/snapshots/components/Popover.test.ts-snapshots/Popover-SX-Props-light-high-contrast-linux.png b/.playwright/snapshots/components/Popover.test.ts-snapshots/Popover-SX-Props-light-high-contrast-linux.png new file mode 100644 index 00000000000..f68e5b80100 Binary files /dev/null and b/.playwright/snapshots/components/Popover.test.ts-snapshots/Popover-SX-Props-light-high-contrast-linux.png differ diff --git a/.playwright/snapshots/components/Popover.test.ts-snapshots/Popover-SX-Props-light-linux.png b/.playwright/snapshots/components/Popover.test.ts-snapshots/Popover-SX-Props-light-linux.png new file mode 100644 index 00000000000..602cb939e3c Binary files /dev/null and b/.playwright/snapshots/components/Popover.test.ts-snapshots/Popover-SX-Props-light-linux.png differ diff --git a/.playwright/snapshots/components/Popover.test.ts-snapshots/Popover-SX-Props-light-tritanopia-linux.png b/.playwright/snapshots/components/Popover.test.ts-snapshots/Popover-SX-Props-light-tritanopia-linux.png new file mode 100644 index 00000000000..602cb939e3c Binary files /dev/null and b/.playwright/snapshots/components/Popover.test.ts-snapshots/Popover-SX-Props-light-tritanopia-linux.png differ diff --git a/e2e/components/Popover.test.ts b/e2e/components/Popover.test.ts index 7a80f28cda4..43ba2962bef 100644 --- a/e2e/components/Popover.test.ts +++ b/e2e/components/Popover.test.ts @@ -2,60 +2,49 @@ import {test, expect} from '@playwright/test' import {visit} from '../test-helpers/storybook' import {themes} from '../test-helpers/themes' -test.describe('Popover', () => { - test.describe('Default', () => { - for (const theme of themes) { - test.describe(theme, () => { - test('default @vrt', async ({page}) => { - await visit(page, { - id: 'components-popover--default', - globals: { - colorScheme: theme, - }, - }) +const stories = [ + { + title: 'Default', + id: 'components-popover--default', + }, + { + title: 'Playground', + id: 'components-popover--playground', + }, + { + title: 'SX Props', + id: 'components-popover-dev--sx-props', + }, +] as const - // Default state - expect(await page.screenshot()).toMatchSnapshot(`Popover.Default.${theme}.png`) - }) - - test('axe @aat', async ({page}) => { - await visit(page, { - id: 'components-popover--default', - globals: { - colorScheme: theme, - }, - }) - await expect(page).toHaveNoViolations() - }) - }) - } - }) +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, + }, + }) - test.describe('Playground', () => { - for (const theme of themes) { - test.describe(theme, () => { - test('default @vrt', async ({page}) => { - await visit(page, { - id: 'components-popover--playground', - globals: { - colorScheme: theme, - }, + // Default state + expect(await page.screenshot()).toMatchSnapshot(`Popover.${story.title}.${theme}.png`) }) - // Default state - expect(await page.screenshot()).toMatchSnapshot(`Popover.Playground.${theme}.png`) - }) - - test('axe @aat', async ({page}) => { - await visit(page, { - id: 'components-popover--playground', - globals: { - colorScheme: theme, - }, + test('axe @aat', async ({page}) => { + await visit(page, { + id: story.id, + globals: { + colorScheme: theme, + }, + }) + await expect(page).toHaveNoViolations() }) - await expect(page).toHaveNoViolations() }) - }) - } - }) + } + }) + } }) diff --git a/packages/react/src/Popover/Popover.dev.stories.tsx b/packages/react/src/Popover/Popover.dev.stories.tsx new file mode 100644 index 00000000000..bbdef11e0b9 --- /dev/null +++ b/packages/react/src/Popover/Popover.dev.stories.tsx @@ -0,0 +1,38 @@ +import React from 'react' +import type {Meta} from '@storybook/react' +import Heading from '../Heading' +import Popover from './Popover' +import Text from '../Text' +import {Button} from '../Button' + +export default { + title: 'Components/Popover/Dev', + component: Popover, +} as Meta + +export const SxProps = () => ( + + + Popover heading + Message about popovers + + + +) diff --git a/packages/react/src/Popover/Popover.module.css b/packages/react/src/Popover/Popover.module.css new file mode 100644 index 00000000000..7ddb4f8cabe --- /dev/null +++ b/packages/react/src/Popover/Popover.module.css @@ -0,0 +1,208 @@ +.Popover { + position: absolute; + z-index: 100; + display: none; + + &:where([data-open]) { + display: block; + } + + &:where([data-relative]) { + position: relative; + } +} + +.PopoverContent { + position: relative; + width: 232px; + padding: var(--base-size-24); + margin-right: auto; + margin-left: auto; + background-color: var(--overlay-bgColor); + border: var(--borderWidth-thin) solid var(--borderColor-default); + border-radius: var(--borderRadius-medium); + + /* Carets */ + &::before, + &::after { + position: absolute; + left: 50%; + display: inline-block; + content: ''; + } + + &::before { + top: calc(-1 * var(--base-size-16)); + /* stylelint-disable-next-line primer/spacing */ + margin-left: -9px; + + /* TODO: solid? */ + /* stylelint-disable-next-line primer/borders */ + border: var(--base-size-8) solid transparent; + border-bottom-color: var(--borderColor-default); + } + + &::after { + /* stylelint-disable-next-line primer/spacing */ + top: -14px; + margin-left: calc(-1 * var(--base-size-8)); + + /* // todo: solid */ + /* stylelint-disable-next-line primer/borders */ + border: 7px solid transparent; + /* stylelint-disable-next-line primer/colors */ + border-bottom-color: var(--overlay-bgColor); + } + + /* Bottom-oriented carets */ + :where([data-caret='bottom']) &, + :where([data-caret='bottom-right']) &, + :where([data-caret='bottom-left']) & { + &::before, + &::after { + top: auto; + border-bottom-color: transparent; + } + + &::before { + bottom: calc(-1 * var(--base-size-16)); + border-top-color: var(--borderColor-default); + } + + &::after { + /* stylelint-disable-next-line primer/spacing */ + bottom: -14px; + /* stylelint-disable-next-line primer/colors */ + border-top-color: var(--overlay-bgColor); + } + } + + /* Top & Bottom: Right-oriented carets */ + :where([data-caret='top-right']) &, + :where([data-caret='bottom-right']) & { + /* stylelint-disable-next-line primer/spacing */ + right: -9px; + margin-right: 0; + + &::before, + &::after { + left: auto; + margin-left: 0; + } + + &::before { + /* stylelint-disable-next-line primer/spacing */ + right: 20px; + } + + &::after { + /* stylelint-disable-next-line primer/spacing */ + right: 21px; + } + } + + /* Top & Bottom: Left-oriented carets */ + :where([data-caret='top-left']) &, + :where([data-caret='bottom-left']) & { + /* stylelint-disable-next-line primer/spacing */ + left: -9px; + margin-left: 0; + + &::before, + &::after { + left: var(--base-size-24); + margin-left: 0; + } + + &::after { + /* stylelint-disable-next-line primer/spacing */ + left: calc(var(--base-size-24) + 1px); + } + } + + /* Right- & Left-oriented carets */ + :where([data-caret='right']) &, + :where([data-caret='right-top']) &, + :where([data-caret='right-bottom']) &, + :where([data-caret='left']) &, + :where([data-caret='left-top']) &, + :where([data-caret='left-bottom']) & { + &::before, + &::after { + top: 50%; + left: auto; + margin-left: 0; + border-bottom-color: transparent; + } + + &::before { + /* stylelint-disable-next-line primer/spacing */ + margin-top: calc((var(--base-size-8) + 1px) * -1); + } + + &::after { + margin-top: calc(-1 * var(--base-size-8)); + } + } + + /* Right-oriented carets */ + :where([data-caret='right']) &, + :where([data-caret='right-top']) &, + :where([data-caret='right-bottom']) & { + &::before { + right: calc(-1 * var(--base-size-16)); + border-left-color: var(--borderColor-default); + } + + &::after { + /* stylelint-disable-next-line primer/spacing */ + right: -14px; + /* stylelint-disable-next-line primer/colors */ + border-left-color: var(--overlay-bgColor); + } + } + + /* Left-oriented carets */ + :where([data-caret='left']) &, + :where([data-caret='left-top']) &, + :where([data-caret='left-bottom']) & { + &::before { + left: calc(-1 * var(--base-size-16)); + border-right-color: var(--borderColor-default); + } + + &::after { + /* stylelint-disable-next-line primer/spacing */ + left: -14px; + /* stylelint-disable-next-line primer/colors */ + border-right-color: var(--overlay-bgColor); + } + } + + /* Right & Left: Top-oriented carets */ + :where([data-caret='right-top']) &, + :where([data-caret='left-top']) & { + &::before, + &::after { + top: var(--base-size-24); + } + } + + /* Right & Left: Bottom-oriented carets */ + :where([data-caret='right-bottom']) &, + :where([data-caret='left-bottom']) & { + &::before, + &::after { + top: auto; + } + + &::before { + bottom: var(--base-size-16); + } + + &::after { + /* stylelint-disable-next-line primer/spacing */ + bottom: calc(var(--base-size-16) + 1px); + } + } +} diff --git a/packages/react/src/Popover/Popover.tsx b/packages/react/src/Popover/Popover.tsx index 711391b338d..3f6c9b3c784 100644 --- a/packages/react/src/Popover/Popover.tsx +++ b/packages/react/src/Popover/Popover.tsx @@ -3,7 +3,11 @@ import styled from 'styled-components' import {get} from '../constants' import type {SxProp} from '../sx' import sx from '../sx' -import type {ComponentProps} from '../utils/types' +import {toggleStyledComponent} from '../internal/utils/toggleStyledComponent' +import {useFeatureFlag} from '../FeatureFlags' +import classes from './Popover.module.css' +import type {HTMLProps} from 'react' +import React from 'react' type CaretPosition = | 'top' @@ -28,7 +32,9 @@ type StyledPopoverProps = { open?: boolean } & SxProp -const Popover = styled.div.attrs(({className, caret = 'top'}) => { +const CSS_MODULES_FLAG = 'primer_react_css_modules_team' + +const StyledPopover = styled.div.attrs(({className, caret = 'top'}) => { return { className: clsx(className, `caret-pos--${caret}`), } @@ -39,187 +45,231 @@ const Popover = styled.div.attrs(({className, caret = 'top'} ${sx}; ` -const PopoverContent = styled.div` - border: 1px solid ${get('colors.border.default')}; - border-radius: ${get('radii.2')}; - position: relative; - width: 232px; - margin-right: auto; - margin-left: auto; - padding: ${get('space.4')}; - background-color: ${get('colors.canvas.overlay')}; - - // Carets - &::before, - &::after { - position: absolute; - left: 50%; - display: inline-block; - content: ''; - } +const BaseComponent = toggleStyledComponent(CSS_MODULES_FLAG, 'div', StyledPopover) - &::before { - top: -${get('space.3')}; - margin-left: -9px; - border: ${get('space.2')} solid transparent; // TODO: solid? - border-bottom-color: ${get('colors.border.default')}; - } +export type PopoverProps = { + /** Class name for custom styling */ + className?: string +} & StyledPopoverProps & + HTMLProps - &::after { - top: -14px; - margin-left: -${get('space.2')}; - border: 7px solid transparent; // todo: solid - border-bottom-color: ${get('colors.canvas.overlay')}; +const Popover: React.FC> = ({ + className, + caret = 'top', + open, + relative, + ...props +}) => { + const enabled = useFeatureFlag(CSS_MODULES_FLAG) + if (enabled) { + return ( + + ) } - // Bottom-oriented carets - ${Popover}.caret-pos--bottom & , - ${Popover}.caret-pos--bottom-right & , - ${Popover}.caret-pos--bottom-left & { + return +} + +const StyledPopoverContent = toggleStyledComponent( + CSS_MODULES_FLAG, + 'div', + styled.div` + border: 1px solid ${get('colors.border.default')}; + border-radius: ${get('radii.2')}; + position: relative; + width: 232px; + margin-right: auto; + margin-left: auto; + padding: ${get('space.4')}; + background-color: ${get('colors.canvas.overlay')}; + + // Carets &::before, &::after { - top: auto; - border-bottom-color: transparent; + position: absolute; + left: 50%; + display: inline-block; + content: ''; } &::before { - bottom: -${get('space.3')}; - border-top-color: ${get('colors.border.default')}; + top: -${get('space.3')}; + margin-left: -9px; + border: ${get('space.2')} solid transparent; // TODO: solid? + border-bottom-color: ${get('colors.border.default')}; } &::after { - bottom: -14px; - // stylelint-disable-next-line primer/borders - border-top-color: ${get('colors.canvas.overlay')}; + top: -14px; + margin-left: -${get('space.2')}; + border: 7px solid transparent; // todo: solid + border-bottom-color: ${get('colors.canvas.overlay')}; } - } - // Top & Bottom: Right-oriented carets - ${Popover}.caret-pos--top-right & , - ${Popover}.caret-pos--bottom-right & { - right: -9px; - margin-right: 0; + // Bottom-oriented carets + ${StyledPopover}.caret-pos--bottom & , + ${StyledPopover}.caret-pos--bottom-right & , + ${StyledPopover}.caret-pos--bottom-left & { + &::before, + &::after { + top: auto; + border-bottom-color: transparent; + } - &::before, - &::after { - left: auto; - margin-left: 0; - } + &::before { + bottom: -${get('space.3')}; + border-top-color: ${get('colors.border.default')}; + } - &::before { - right: 20px; + &::after { + bottom: -14px; + // stylelint-disable-next-line primer/borders + border-top-color: ${get('colors.canvas.overlay')}; + } } - &::after { - right: 21px; - } - } + // Top & Bottom: Right-oriented carets + ${StyledPopover}.caret-pos--top-right & , + ${StyledPopover}.caret-pos--bottom-right & { + right: -9px; + margin-right: 0; - // Top & Bottom: Left-oriented carets - ${Popover}.caret-pos--top-left & , - ${Popover}.caret-pos--bottom-left & { - left: -9px; - margin-left: 0; + &::before, + &::after { + left: auto; + margin-left: 0; + } - &::before, - &::after { - left: ${get('space.4')}; - margin-left: 0; - } + &::before { + right: 20px; + } - &::after { - left: calc(${get('space.4')} + 1px); + &::after { + right: 21px; + } } - } - // Right- & Left-oriented carets - ${Popover}.caret-pos--right & , - ${Popover}.caret-pos--right-top & , - ${Popover}.caret-pos--right-bottom & , - ${Popover}.caret-pos--left & , - ${Popover}.caret-pos--left-top & , - ${Popover}.caret-pos--left-bottom & { - &::before, - &::after { - top: 50%; - left: auto; + // Top & Bottom: Left-oriented carets + ${StyledPopover}.caret-pos--top-left & , + ${StyledPopover}.caret-pos--bottom-left & { + left: -9px; margin-left: 0; - border-bottom-color: transparent; - } - &::before { - // stylelint-disable-next-line primer/spacing - margin-top: calc((${get('space.2')} + 1px) * -1); - } + &::before, + &::after { + left: ${get('space.4')}; + margin-left: 0; + } - &::after { - margin-top: -${get('space.2')}; + &::after { + left: calc(${get('space.4')} + 1px); + } } - } - // Right-oriented carets - ${Popover}.caret-pos--right & , - ${Popover}.caret-pos--right-top & , - ${Popover}.caret-pos--right-bottom & { - &::before { - right: -${get('space.3')}; - border-left-color: ${get('colors.border.default')}; - } + // Right- & Left-oriented carets + ${StyledPopover}.caret-pos--right & , + ${StyledPopover}.caret-pos--right-top & , + ${StyledPopover}.caret-pos--right-bottom & , + ${StyledPopover}.caret-pos--left & , + ${StyledPopover}.caret-pos--left-top & , + ${StyledPopover}.caret-pos--left-bottom & { + &::before, + &::after { + top: 50%; + left: auto; + margin-left: 0; + border-bottom-color: transparent; + } - &::after { - right: -14px; - // stylelint-disable-next-line primer/borders - border-left-color: ${get('colors.canvas.overlay')}; - } - } + &::before { + // stylelint-disable-next-line primer/spacing + margin-top: calc((${get('space.2')} + 1px) * -1); + } - // Left-oriented carets - ${Popover}.caret-pos--left & , - ${Popover}.caret-pos--left-top & , - ${Popover}.caret-pos--left-bottom & { - &::before { - left: -${get('space.3')}; - border-right-color: ${get('colors.border.default')}; + &::after { + margin-top: -${get('space.2')}; + } } - &::after { - left: -14px; - // stylelint-disable-next-line primer/borders - border-right-color: ${get('colors.canvas.overlay')}; - } - } + // Right-oriented carets + ${StyledPopover}.caret-pos--right & , + ${StyledPopover}.caret-pos--right-top & , + ${StyledPopover}.caret-pos--right-bottom & { + &::before { + right: -${get('space.3')}; + border-left-color: ${get('colors.border.default')}; + } - // Right & Left: Top-oriented carets - ${Popover}.caret-pos--right-top & , - ${Popover}.caret-pos--left-top & { - &::before, - &::after { - top: ${get('space.4')}; + &::after { + right: -14px; + // stylelint-disable-next-line primer/borders + border-left-color: ${get('colors.canvas.overlay')}; + } } - } - // Right & Left: Bottom-oriented carets - ${Popover}.caret-pos--right-bottom & , - ${Popover}.caret-pos--left-bottom & { - &::before, - &::after { - top: auto; + // Left-oriented carets + ${StyledPopover}.caret-pos--left & , + ${StyledPopover}.caret-pos--left-top & , + ${StyledPopover}.caret-pos--left-bottom & { + &::before { + left: -${get('space.3')}; + border-right-color: ${get('colors.border.default')}; + } + + &::after { + left: -14px; + // stylelint-disable-next-line primer/borders + border-right-color: ${get('colors.canvas.overlay')}; + } } - &::before { - bottom: ${get('space.3')}; + // Right & Left: Top-oriented carets + ${StyledPopover}.caret-pos--right-top & , + ${StyledPopover}.caret-pos--left-top & { + &::before, + &::after { + top: ${get('space.4')}; + } } - &::after { - bottom: calc(${get('space.3')} + 1px); + // Right & Left: Bottom-oriented carets + ${StyledPopover}.caret-pos--right-bottom & , + ${StyledPopover}.caret-pos--left-bottom & { + &::before, + &::after { + top: auto; + } + + &::before { + bottom: ${get('space.3')}; + } + + &::after { + bottom: calc(${get('space.3')} + 1px); + } } + + ${sx}; + `, +) + +export type PopoverContentProps = {className?: string} & StyledPopoverProps & HTMLProps + +const PopoverContent: React.FC> = ({className, ...props}) => { + const enabled = useFeatureFlag(CSS_MODULES_FLAG) + if (enabled) { + return } - ${sx}; -` + return +} PopoverContent.displayName = 'Popover.Content' -export type PopoverProps = ComponentProps -export type PopoverContentProps = ComponentProps export default Object.assign(Popover, {Content: PopoverContent})