From 79b9fdb1f8d139c4492e39ea9e474a753af17154 Mon Sep 17 00:00:00 2001 From: Cat Johnson <123020281+catandthemachines@users.noreply.github.com> Date: Wed, 21 Aug 2024 14:40:23 -0700 Subject: [PATCH] Update tooltip-popper to handle smaller screensizes. (#2271) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary: Adjusting implementation of Popper in tooltip-popper to handle smaller screen sizes by: - Adjusting location of popper based on document size, not viewport size. This will ensure the popper doesn't default to inaccessible locations when the popper doesn't fit in the viewport size. - Disabled isReferenceHidden flag to ensure the popper stays present even when the reference anchor is out of view. This will allow users in higher zoom levels or smaller screen sizes to scroll to interact with the popper if necessary instead of it disappearing when the reference isn't in view. Issue: LEMS-2085 ## Test plan: - Go to url: https://khan.github.io/wonder-blocks/iframe.html?args=&id=packages-popover-popover--with-actions&viewMode=story - Set the page body to have padding: "0 0 500px 0" to ensure the page body is larger than the height of the button example. - Set the viewport size to 320 x 256 - You should see the popover attach at the bottom of the trigger element since there is more room within the page document. Author: catandthemachines Reviewers: catandthemachines, handeyeco, jandrade, anakaren-rojas Required Reviewers: Approved By: jandrade Checks: ✅ codecov/project, ✅ Chromatic - Get results on regular PRs (ubuntu-latest, 20.x), ✅ Test (ubuntu-latest, 20.x, 2/2), ✅ Check build sizes (ubuntu-latest, 20.x), ✅ Test (ubuntu-latest, 20.x, 1/2), ✅ Lint (ubuntu-latest, 20.x), ✅ Chromatic - Build on regular PRs / chromatic (ubuntu-latest, 20.x), ✅ Publish npm snapshot (ubuntu-latest, 20.x), ⏭️ Chromatic - Skip on Release PR (changesets), ✅ Prime node_modules cache for primary configuration (ubuntu-latest, 20.x), ✅ Check for .changeset entries for all changed files (ubuntu-latest, 20.x), ✅ gerald, ⏭️ dependabot Pull Request URL: https://github.com/Khan/wonder-blocks/pull/2271 --- .changeset/kind-socks-report.md | 8 ++ .../popover.argtypes.tsx | 11 +++ .../wonder-blocks-popover/popover.stories.tsx | 36 ++++++++ .../src/components/popover.tsx | 17 +++- .../src/components/tooltip-popper.tsx | 92 ++++++++++++++++--- 5 files changed, 152 insertions(+), 12 deletions(-) create mode 100644 .changeset/kind-socks-report.md diff --git a/.changeset/kind-socks-report.md b/.changeset/kind-socks-report.md new file mode 100644 index 000000000..8202c9cbf --- /dev/null +++ b/.changeset/kind-socks-report.md @@ -0,0 +1,8 @@ +--- +"@khanacademy/wonder-blocks-tooltip": minor +--- + +Update tooltip implementation of Popper to: + +- Ensure that the popper doesn't disappear when the referenced element is not in view in very small screen sizes. This ensures customers can interact with the popper in extra small screen sizes or +400% zoom without the popper randomly disappearing. +- Addition of an optional property to set what the root boundary is for the popper behavior. This is set to "viewport" by default, causing the popper to be positioned based on the user's viewport. If set to "document", it will position itself based on where there is available room within the document body \ No newline at end of file diff --git a/__docs__/wonder-blocks-popover/popover.argtypes.tsx b/__docs__/wonder-blocks-popover/popover.argtypes.tsx index 5444bb824..e82f81e96 100644 --- a/__docs__/wonder-blocks-popover/popover.argtypes.tsx +++ b/__docs__/wonder-blocks-popover/popover.argtypes.tsx @@ -122,6 +122,17 @@ export default { type: "boolean", }, }, + rootBoundary: { + description: + `Optional property to set what the root boundary is for the popper behavior. ` + + `This is set to "viewport" by default, causing the popper to be positioned based ` + + `on the user's viewport. If set to "document", it will position itself based ` + + `on where there is available room within the document body.`, + control: { + type: "select", + options: ["viewport", "document"], + }, + }, portal: { description: `Optional property to enable the portal functionality of popover.` + diff --git a/__docs__/wonder-blocks-popover/popover.stories.tsx b/__docs__/wonder-blocks-popover/popover.stories.tsx index 05ebdc7e9..6c613dcad 100644 --- a/__docs__/wonder-blocks-popover/popover.stories.tsx +++ b/__docs__/wonder-blocks-popover/popover.stories.tsx @@ -735,6 +735,42 @@ export const PopoverAlignment: StoryComponentType = { ), }; +export const WithDocumentRootBoundary: StoryComponentType = () => { + return ( + + ( + + + + } + /> + )} + placement="top" + > + + + + ); +}; + +WithDocumentRootBoundary.parameters = { + docs: { + description: { + story: `Sometimes you need to change the underlining behavior to position the + Popover by the whole webpage (document) instead of by the viewport. This is a + useful tool for popovers with large content that might not fit in small screen + sizes or at 400% zoom. For this reason, you can make use of the + \`rootBoundary\` prop:`, + }, + }, +}; + /** * With custom aria-label - overrides the default aria-labelledby */ diff --git a/packages/wonder-blocks-popover/src/components/popover.tsx b/packages/wonder-blocks-popover/src/components/popover.tsx index 077c32e32..ef8370b28 100644 --- a/packages/wonder-blocks-popover/src/components/popover.tsx +++ b/packages/wonder-blocks-popover/src/components/popover.tsx @@ -10,6 +10,7 @@ import type { Placement, PopperElementProps, } from "@khanacademy/wonder-blocks-tooltip"; +import type {RootBoundary} from "@popperjs/core"; import PopoverContent from "./popover-content"; import PopoverContentCore from "./popover-content-core"; @@ -116,6 +117,13 @@ type Props = AriaProps & * your content does not get clipped or hidden. */ portal?: boolean; + /** + * Optional property to set what the root boundary is for the popper behavior. + * This is set to "viewport" by default, causing the popper to be positioned based + * on the user's viewport. If set to "document", it will position itself based + * on where there is available room within the document body. + */ + rootBoundary?: RootBoundary; }>; type State = Readonly<{ @@ -137,6 +145,7 @@ type DefaultProps = Readonly<{ placement: Props["placement"]; showTail: Props["showTail"]; portal: Props["portal"]; + rootBoundary: Props["rootBoundary"]; }>; /** @@ -167,6 +176,7 @@ export default class Popover extends React.Component { placement: "top", showTail: true, portal: true, + rootBoundary: "viewport", }; /** @@ -279,6 +289,7 @@ export default class Popover extends React.Component { portal, "aria-label": ariaLabel, "aria-describedby": ariaDescribedBy, + rootBoundary, } = this.props; const {anchorElement} = this.state; @@ -287,7 +298,11 @@ export default class Popover extends React.Component { const ariaLabelledBy = ariaLabel ? undefined : `${uniqueId}-title`; const popperContent = ( - + {(props: PopperElementProps) => ( >; + +type Modifiers = + | Partial + | Partial + | Partial; + +/** + * This function calculates the height of the popper + * vs. the height of the viewport. If the popper is larger + * than the viewport, it sets the popper isReferenceHidden + * state to false, to ensure the popper stays visible even if + * the reference is no longer in view. If the popper is less + * than the viewport, it leaves it as is so the popper will + * disappear if the reference is no longer in view. + */ +function _modifyPosition({ + state, +}: ModifierArguments>): void { + // Calculates the available space for the popper based on the placement + // relative to the viewport. + const popperHeight = + state.rects.popper.height + state.rects.reference.height; + const minHeight = document.documentElement.clientHeight; + + if (minHeight < popperHeight && state.modifiersData.hide) { + state.modifiersData.hide = { + ...state.modifiersData.hide, + isReferenceHidden: false, + }; + } +} + +const smallViewportModifier: SmallViewportModifier = { + name: "smallViewport", + enabled: true, + phase: "main", + fn: _modifyPosition, +}; + /** * A component that wraps react-popper's Popper component to provide a * consistent interface for positioning floating elements. */ export default class TooltipPopper extends React.Component { + static defaultProps: DefaultProps = { + rootBoundary: "viewport", + }; + /** * Automatically updates the position of the floating element when necessary * to ensure it stays anchored. @@ -172,21 +230,33 @@ export default class TooltipPopper extends React.Component { } render(): React.ReactNode { - const {anchorElement, placement} = this.props; + const {anchorElement, placement, rootBoundary} = this.props; + + // TODO(WB-1680): Use floating-ui's + const modifiers: Modifiers[] = [smallViewportModifier]; + + if (rootBoundary === "viewport") { + modifiers.push({ + name: "preventOverflow", + options: { + rootBoundary: "viewport", + }, + }); + } else { + modifiers.push({ + name: "flip", + options: { + rootBoundary: "document", + }, + }); + } return ( {(props) => this._renderPositionedContent(props)}