Skip to content

Commit

Permalink
Update tooltip-popper to handle smaller screensizes. (#2271)
Browse files Browse the repository at this point in the history
## 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: #2271
  • Loading branch information
catandthemachines authored Aug 21, 2024
1 parent 5161f2c commit 79b9fdb
Show file tree
Hide file tree
Showing 5 changed files with 152 additions and 12 deletions.
8 changes: 8 additions & 0 deletions .changeset/kind-socks-report.md
Original file line number Diff line number Diff line change
@@ -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
11 changes: 11 additions & 0 deletions __docs__/wonder-blocks-popover/popover.argtypes.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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.` +
Expand Down
36 changes: 36 additions & 0 deletions __docs__/wonder-blocks-popover/popover.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -735,6 +735,42 @@ export const PopoverAlignment: StoryComponentType = {
),
};

export const WithDocumentRootBoundary: StoryComponentType = () => {
return (
<View style={{paddingBottom: "500px"}}>
<Popover
rootBoundary="document"
content={() => (
<PopoverContent
title="Popover with rootBoundary='document'"
content="This example shows a popover with the rootBoundary='document'. This means that instead of aligning the popover to the viewport, it will instead place the popover where there is room in the DOM. This is a useful tool for popovers with large content that might not fit in small screen sizes or at 400% zoom."
actions={
<View style={[styles.row, styles.actions]}>
<Strut size={spacing.medium_16} />
</View>
}
/>
)}
placement="top"
>
<Button>Open popover with document rootBoundary</Button>
</Popover>
</View>
);
};

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
*/
Expand Down
17 changes: 16 additions & 1 deletion packages/wonder-blocks-popover/src/components/popover.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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<{
Expand All @@ -137,6 +145,7 @@ type DefaultProps = Readonly<{
placement: Props["placement"];
showTail: Props["showTail"];
portal: Props["portal"];
rootBoundary: Props["rootBoundary"];
}>;

/**
Expand Down Expand Up @@ -167,6 +176,7 @@ export default class Popover extends React.Component<Props, State> {
placement: "top",
showTail: true,
portal: true,
rootBoundary: "viewport",
};

/**
Expand Down Expand Up @@ -279,6 +289,7 @@ export default class Popover extends React.Component<Props, State> {
portal,
"aria-label": ariaLabel,
"aria-describedby": ariaDescribedBy,
rootBoundary,
} = this.props;
const {anchorElement} = this.state;

Expand All @@ -287,7 +298,11 @@ export default class Popover extends React.Component<Props, State> {
const ariaLabelledBy = ariaLabel ? undefined : `${uniqueId}-title`;

const popperContent = (
<TooltipPopper anchorElement={anchorElement} placement={placement}>
<TooltipPopper
anchorElement={anchorElement}
placement={placement}
rootBoundary={rootBoundary}
>
{(props: PopperElementProps) => (
<PopoverDialog
{...props}
Expand Down
92 changes: 81 additions & 11 deletions packages/wonder-blocks-tooltip/src/components/tooltip-popper.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,18 @@
*/
import * as React from "react";
import {Popper} from "react-popper";
import type {PopperChildrenProps} from "react-popper";
import type {Modifier, PopperChildrenProps} from "react-popper";

import {UnreachableCaseError} from "@khanacademy/wonder-stuff-core";
import RefTracker from "../util/ref-tracker";
import type {ModifierArguments, RootBoundary} from "@popperjs/core";
import type {FlipModifier} from "@popperjs/core/lib/modifiers/flip";
import type {PreventOverflowModifier} from "@popperjs/core/lib/modifiers/preventOverflow";
import type {
Placement,
PopperElementProps,
PopperUpdateFn,
} from "../util/types";
import RefTracker from "../util/ref-tracker";

type Props = {
/**
Expand All @@ -34,6 +37,17 @@ type Props = {
* anchor element changes.
*/
autoUpdate?: 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 DefaultProps = {
rootBoundary: Props["rootBoundary"];
};

const filterPopperPlacement = (
Expand Down Expand Up @@ -64,11 +78,55 @@ const filterPopperPlacement = (
}
};

type SmallViewportModifier = Modifier<"smallViewport", Record<string, never>>;

type Modifiers =
| Partial<PreventOverflowModifier>
| Partial<FlipModifier>
| Partial<SmallViewportModifier>;

/**
* 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<Record<string, never>>): 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<Props> {
static defaultProps: DefaultProps = {
rootBoundary: "viewport",
};

/**
* Automatically updates the position of the floating element when necessary
* to ensure it stays anchored.
Expand Down Expand Up @@ -172,21 +230,33 @@ export default class TooltipPopper extends React.Component<Props> {
}

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 (
<Popper
referenceElement={anchorElement}
strategy="fixed"
placement={placement}
modifiers={[
{
name: "preventOverflow",
options: {
rootBoundary: "viewport",
},
},
]}
modifiers={modifiers}
>
{(props) => this._renderPositionedContent(props)}
</Popper>
Expand Down

0 comments on commit 79b9fdb

Please sign in to comment.