diff --git a/.changeset/strange-beans-grow.md b/.changeset/strange-beans-grow.md new file mode 100644 index 00000000000..032e8ed5752 --- /dev/null +++ b/.changeset/strange-beans-grow.md @@ -0,0 +1,5 @@ +--- +'@shopify/polaris': minor +--- + +Added optional `captureOverscroll` prop to `Popover` diff --git a/polaris-react/src/components/Popover/Popover.scss b/polaris-react/src/components/Popover/Popover.scss index 1bae452adf8..399e73a8cc1 100644 --- a/polaris-react/src/components/Popover/Popover.scss +++ b/polaris-react/src/components/Popover/Popover.scss @@ -106,6 +106,10 @@ $vertical-motion-offset: -5px; flex: 0 0 auto; } +.Pane-captureOverscroll { + overscroll-behavior: contain; +} + .Section { padding: var(--p-space-4); diff --git a/polaris-react/src/components/Popover/Popover.tsx b/polaris-react/src/components/Popover/Popover.tsx index be376dc87f1..dfe7e7efc9c 100644 --- a/polaris-react/src/components/Popover/Popover.tsx +++ b/polaris-react/src/components/Popover/Popover.tsx @@ -78,6 +78,11 @@ export interface PopoverProps { autofocusTarget?: PopoverAutofocusTarget; /** Prevents closing the popover when other overlays are clicked */ preventCloseOnChildOverlayClick?: boolean; + /** + * Prevents page scrolling when the end of the scrollable Popover overlay content is reached - applied to Pane subcomponent + * @default false + */ + captureOverscroll?: boolean; } export interface PopoverPublicAPI { diff --git a/polaris-react/src/components/Popover/components/Pane/Pane.tsx b/polaris-react/src/components/Popover/components/Pane/Pane.tsx index 491ae742e2a..2ee7d084097 100644 --- a/polaris-react/src/components/Popover/components/Pane/Pane.tsx +++ b/polaris-react/src/components/Popover/components/Pane/Pane.tsx @@ -17,16 +17,26 @@ export interface PaneProps { height?: string; /** Callback when the bottom of the popover is reached by mouse or keyboard */ onScrolledToBottom?(): void; + /** + * Prevents page scrolling when the end of the scrollable Popover content is reached + * @default false + */ + captureOverscroll?: boolean; } export function Pane({ + captureOverscroll = false, fixed, sectioned, children, height, onScrolledToBottom, }: PaneProps) { - const className = classNames(styles.Pane, fixed && styles['Pane-fixed']); + const className = classNames( + styles.Pane, + fixed && styles['Pane-fixed'], + captureOverscroll && styles['Pane-captureOverscroll'], + ); const content = sectioned ? wrapWithComponent(children, Section, {}) : children; diff --git a/polaris-react/src/components/Popover/components/Pane/tests/Pane.test.tsx b/polaris-react/src/components/Popover/components/Pane/tests/Pane.test.tsx index 8fbde51822f..83ebd471195 100644 --- a/polaris-react/src/components/Popover/components/Pane/tests/Pane.test.tsx +++ b/polaris-react/src/components/Popover/components/Pane/tests/Pane.test.tsx @@ -153,4 +153,54 @@ describe('', () => { }); }); }); + + describe('captureOverscroll', () => { + const Children = () => ( + +

Text

+
+ ); + + describe('when not passed', () => { + it('does not apply the Pane-captureOverscroll class', () => { + const popoverPane = mountWithApp( + + + , + ); + + expect(popoverPane).toContainReactComponent(Scrollable, { + className: 'Pane', + }); + }); + }); + + describe('when passed as true', () => { + it('applies the Pane-captureOverscroll class', () => { + const popoverPane = mountWithApp( + + + , + ); + + expect(popoverPane).toContainReactComponent(Scrollable, { + className: 'Pane Pane-captureOverscroll', + }); + }); + }); + + describe('when passed as false', () => { + it('does not apply the Pane-captureOverscroll class', () => { + const popoverPane = mountWithApp( + + + , + ); + + expect(popoverPane).toContainReactComponent(Scrollable, { + className: 'Pane', + }); + }); + }); + }); }); diff --git a/polaris-react/src/components/Popover/components/PopoverOverlay/PopoverOverlay.tsx b/polaris-react/src/components/Popover/components/PopoverOverlay/PopoverOverlay.tsx index 862a076fc91..5fd3fb9a1f6 100644 --- a/polaris-react/src/components/Popover/components/PopoverOverlay/PopoverOverlay.tsx +++ b/polaris-react/src/components/Popover/components/PopoverOverlay/PopoverOverlay.tsx @@ -57,6 +57,7 @@ export interface PopoverOverlayProps { onClose(source: PopoverCloseSource): void; autofocusTarget?: PopoverAutofocusTarget; preventCloseOnChildOverlayClick?: boolean; + captureOverscroll?: boolean; } interface State { @@ -222,6 +223,7 @@ export class PopoverOverlay extends PureComponent { fluidContent, hideOnPrint, autofocusTarget, + captureOverscroll, } = this.props; const className = classNames( @@ -248,7 +250,7 @@ export class PopoverOverlay extends PureComponent { style={contentStyles} ref={this.contentNode} > - {renderPopoverContent(children, {sectioned})} + {renderPopoverContent(children, {captureOverscroll, sectioned})} ); diff --git a/polaris-react/src/components/Popover/tests/Popover.test.tsx b/polaris-react/src/components/Popover/tests/Popover.test.tsx index 56be705a1cb..46e9ae3ee34 100644 --- a/polaris-react/src/components/Popover/tests/Popover.test.tsx +++ b/polaris-react/src/components/Popover/tests/Popover.test.tsx @@ -5,8 +5,9 @@ import {Portal} from '../../Portal'; import {PositionedOverlay} from '../../PositionedOverlay'; import {Popover} from '../Popover'; import type {PopoverPublicAPI} from '../Popover'; -import {PopoverOverlay} from '../components'; +import {Pane, PopoverOverlay} from '../components'; import * as setActivatorAttributes from '../set-activator-attributes'; +import {TextContainer} from '../../TextContainer'; describe('', () => { const spy = jest.fn(); @@ -368,6 +369,64 @@ describe('', () => { }); }); }); + + describe('captureOverscroll', () => { + const TestActivator = ; + + const Children = () => ( + +

Text

+
+ ); + + const defaultProps = { + active: true, + activator: TestActivator, + onClose: jest.fn(), + }; + + describe('when not passed', () => { + it('does not pass the prop as true to the Pane component', () => { + const popover = mountWithApp( + + + , + ); + + expect(popover).toContainReactComponent(Pane, { + captureOverscroll: undefined, + }); + }); + }); + + describe('when passed as true', () => { + it('passes the prop as true to the Pane component', () => { + const popover = mountWithApp( + + + , + ); + + expect(popover).toContainReactComponent(Pane, { + captureOverscroll: true, + }); + }); + }); + + describe('when passed as false', () => { + it('passes the prop as false to the Pane component', () => { + const popover = mountWithApp( + + + , + ); + + expect(popover).toContainReactComponent(Pane, { + captureOverscroll: false, + }); + }); + }); + }); }); function noop() {}