diff --git a/.changeset/chilled-rules-boil.md b/.changeset/chilled-rules-boil.md new file mode 100644 index 00000000000..f1b42e7431c --- /dev/null +++ b/.changeset/chilled-rules-boil.md @@ -0,0 +1,5 @@ +--- +"@primer/react": minor +--- + +Feat: popover implement click outside diff --git a/.playwright/snapshots/components/Popover.test.ts-snapshots/Popover-Close-On-Click-Outside-dark-colorblind-linux.png b/.playwright/snapshots/components/Popover.test.ts-snapshots/Popover-Close-On-Click-Outside-dark-colorblind-linux.png new file mode 100644 index 00000000000..6b14b408988 Binary files /dev/null and b/.playwright/snapshots/components/Popover.test.ts-snapshots/Popover-Close-On-Click-Outside-dark-colorblind-linux.png differ diff --git a/.playwright/snapshots/components/Popover.test.ts-snapshots/Popover-Close-On-Click-Outside-dark-dimmed-linux.png b/.playwright/snapshots/components/Popover.test.ts-snapshots/Popover-Close-On-Click-Outside-dark-dimmed-linux.png new file mode 100644 index 00000000000..a51d82201d3 Binary files /dev/null and b/.playwright/snapshots/components/Popover.test.ts-snapshots/Popover-Close-On-Click-Outside-dark-dimmed-linux.png differ diff --git a/.playwright/snapshots/components/Popover.test.ts-snapshots/Popover-Close-On-Click-Outside-dark-high-contrast-linux.png b/.playwright/snapshots/components/Popover.test.ts-snapshots/Popover-Close-On-Click-Outside-dark-high-contrast-linux.png new file mode 100644 index 00000000000..07ad3ee495e Binary files /dev/null and b/.playwright/snapshots/components/Popover.test.ts-snapshots/Popover-Close-On-Click-Outside-dark-high-contrast-linux.png differ diff --git a/.playwright/snapshots/components/Popover.test.ts-snapshots/Popover-Close-On-Click-Outside-dark-linux.png b/.playwright/snapshots/components/Popover.test.ts-snapshots/Popover-Close-On-Click-Outside-dark-linux.png new file mode 100644 index 00000000000..6b14b408988 Binary files /dev/null and b/.playwright/snapshots/components/Popover.test.ts-snapshots/Popover-Close-On-Click-Outside-dark-linux.png differ diff --git a/.playwright/snapshots/components/Popover.test.ts-snapshots/Popover-Close-On-Click-Outside-dark-tritanopia-linux.png b/.playwright/snapshots/components/Popover.test.ts-snapshots/Popover-Close-On-Click-Outside-dark-tritanopia-linux.png new file mode 100644 index 00000000000..6b14b408988 Binary files /dev/null and b/.playwright/snapshots/components/Popover.test.ts-snapshots/Popover-Close-On-Click-Outside-dark-tritanopia-linux.png differ diff --git a/.playwright/snapshots/components/Popover.test.ts-snapshots/Popover-Close-On-Click-Outside-light-colorblind-linux.png b/.playwright/snapshots/components/Popover.test.ts-snapshots/Popover-Close-On-Click-Outside-light-colorblind-linux.png new file mode 100644 index 00000000000..ca51fb4df02 Binary files /dev/null and b/.playwright/snapshots/components/Popover.test.ts-snapshots/Popover-Close-On-Click-Outside-light-colorblind-linux.png differ diff --git a/.playwright/snapshots/components/Popover.test.ts-snapshots/Popover-Close-On-Click-Outside-light-high-contrast-linux.png b/.playwright/snapshots/components/Popover.test.ts-snapshots/Popover-Close-On-Click-Outside-light-high-contrast-linux.png new file mode 100644 index 00000000000..8f4b3d9065e Binary files /dev/null and b/.playwright/snapshots/components/Popover.test.ts-snapshots/Popover-Close-On-Click-Outside-light-high-contrast-linux.png differ diff --git a/.playwright/snapshots/components/Popover.test.ts-snapshots/Popover-Close-On-Click-Outside-light-linux.png b/.playwright/snapshots/components/Popover.test.ts-snapshots/Popover-Close-On-Click-Outside-light-linux.png new file mode 100644 index 00000000000..ca51fb4df02 Binary files /dev/null and b/.playwright/snapshots/components/Popover.test.ts-snapshots/Popover-Close-On-Click-Outside-light-linux.png differ diff --git a/.playwright/snapshots/components/Popover.test.ts-snapshots/Popover-Close-On-Click-Outside-light-tritanopia-linux.png b/.playwright/snapshots/components/Popover.test.ts-snapshots/Popover-Close-On-Click-Outside-light-tritanopia-linux.png new file mode 100644 index 00000000000..ca51fb4df02 Binary files /dev/null and b/.playwright/snapshots/components/Popover.test.ts-snapshots/Popover-Close-On-Click-Outside-light-tritanopia-linux.png differ diff --git a/e2e/components/Popover.test.ts b/e2e/components/Popover.test.ts index 7407287d8e1..5d83732603b 100644 --- a/e2e/components/Popover.test.ts +++ b/e2e/components/Popover.test.ts @@ -11,6 +11,10 @@ const stories = [ title: 'Playground', id: 'components-popover--playground', }, + { + title: 'Close On Click Outside', + id: 'components-popover-features--close-on-click-outside', + }, ] as const test.describe('Popover', () => { diff --git a/packages/react/src/PageLayout/PageLayout.module.css b/packages/react/src/PageLayout/PageLayout.module.css index 453ccba6ddb..d8cbee7e572 100644 --- a/packages/react/src/PageLayout/PageLayout.module.css +++ b/packages/react/src/PageLayout/PageLayout.module.css @@ -70,6 +70,9 @@ body[data-page-layout-dragging='true'] * { margin-right: auto; margin-left: auto; flex-wrap: wrap; + /* the wrapper should match the Root's dimensions by default */ + width: 100%; + height: 100%; &:where([data-width='medium']) { max-width: 768px; diff --git a/packages/react/src/Popover/Popover.docs.json b/packages/react/src/Popover/Popover.docs.json index 1e257cd3b9b..7953c7ab052 100644 --- a/packages/react/src/Popover/Popover.docs.json +++ b/packages/react/src/Popover/Popover.docs.json @@ -62,6 +62,16 @@ "type": "| 'auto' | 'hidden' | 'scroll' | 'visible'", "defaultValue": "'auto'", "description": "Sets the overflow behavior of the popover content." + }, + { + "name": "onClickOutside", + "type": "(event: MouseEvent | TouchEvent) => void", + "description": "Callback fired when a click is detected outside the popover content" + }, + { + "name": "ignoreClickRefs", + "type": "React.RefObject[]", + "description": "Refs to elements that should be ignored when detecting outside clicks" } ] } diff --git a/packages/react/src/Popover/Popover.features.stories.tsx b/packages/react/src/Popover/Popover.features.stories.tsx new file mode 100644 index 00000000000..c5ca2b380cf --- /dev/null +++ b/packages/react/src/Popover/Popover.features.stories.tsx @@ -0,0 +1,38 @@ +import type {Meta} from '@storybook/react-vite' +import Heading from '../Heading' +import Popover from './Popover' +import Text from '../Text' +import {Button} from '../Button' +import React from 'react' + +export default { + title: 'Components/Popover/Features', + component: Popover, +} as Meta + +export const CloseOnClickOutside = () => { + const [open, setOpen] = React.useState(true) + const buttonRef = React.useRef(null) + return ( + <> + + + setOpen(false)} + ignoreClickRefs={[buttonRef]} + > + Popover heading + Message about popovers + + + + + ) +} diff --git a/packages/react/src/Popover/Popover.tsx b/packages/react/src/Popover/Popover.tsx index 80e877afba4..6eb3a0d9fea 100644 --- a/packages/react/src/Popover/Popover.tsx +++ b/packages/react/src/Popover/Popover.tsx @@ -1,7 +1,11 @@ import {clsx} from 'clsx' import classes from './Popover.module.css' import type {HTMLProps} from 'react' -import React from 'react' +import React, {useRef} from 'react' +import {useOnOutsideClick} from '../hooks' + +// Stable empty array reference to avoid unnecessary re-renders +const EMPTY_IGNORE_CLICK_REFS: React.RefObject[] = [] type CaretPosition = | 'top' @@ -51,15 +55,43 @@ export type PopoverContentProps = { width?: 'xsmall' | 'small' | 'large' | 'medium' | 'auto' | 'xlarge' height?: 'small' | 'large' | 'medium' | 'auto' | 'xlarge' | 'fit-content' overflow?: 'auto' | 'hidden' | 'scroll' | 'visible' + /* + * Callback fired when a click is detected outside the popover content + */ + onClickOutside?: (event: MouseEvent | TouchEvent) => void + /* + * Refs to elements that should be ignored when detecting outside clicks + */ + ignoreClickRefs?: React.RefObject[] } & HTMLProps const PopoverContent: React.FC> = ({ className, width = 'small', height = 'fit-content', + onClickOutside, + ignoreClickRefs, ...props }) => { - return
+ const divRef = useRef(null) + + const outsideClickHandler = onClickOutside ?? (() => {}) + + useOnOutsideClick({ + onClickOutside: outsideClickHandler, + containerRef: divRef, + ignoreClickRefs: ignoreClickRefs ?? EMPTY_IGNORE_CLICK_REFS, + }) + + return ( +
+ ) } PopoverContent.displayName = 'Popover.Content'