Skip to content

Commit

Permalink
fix: correct arrow position when enabling popper's flip modifier (#761)
Browse files Browse the repository at this point in the history
* fix: correct arrow position when enabling popper's flip modifier

* feat: combining default and user-defined modifiers

* docs: update the commonly asked questions in popover and tooltip
  • Loading branch information
cheton authored May 18, 2023
1 parent 090364c commit 0ce5d4e
Show file tree
Hide file tree
Showing 11 changed files with 369 additions and 104 deletions.
84 changes: 80 additions & 4 deletions packages/react-docs/pages/components/popover.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -459,23 +459,99 @@ Use the `placement` prop to control the placement of the popover.

## Commonly Asked Questions

### Preventing popover content cut-off with `PopperProps`
### Resolving popover content cut-off with `PopperProps`

By default, the `Popover` component positions the popover relative to its parent container. In some cases, the popover content might be cut off when it extends outside the container that holds it.

To mitigate this issue, you can pass `PopperProps={{ usePortal: true }}` to `PopoverContent` to make it positioned on the document root.
To address this issue, you can pass `PopperProps={{ usePortal: true }}` to `PopoverContent` to make it positioned on the document root.

```jsx
<Popover>
<PopoverTrigger>
<Button variant="secondary">Trigger</Button>
</PopoverTrigger>
<PopoverContent PopperProps={{ usePortal: true }}>
<PopoverContent
PopperProps={{
usePortal: true,
}}
>
Popover
</PopoverContent>
</Popover>
```

### Automatically adjusting popover placement with the `flip` modifier

The `flip` modifier is a useful feature that allows for automatic adjustment of popover placement when it is at risk of overflowing the specified boundary. To learn more about utilizing the `flip` modifier, please refer to [Popper.js documentation](https://popper.js.org/docs/v2/modifiers/flip/).

In the following example, the popover's placement is initially set to `top`. However, if the placement is not suitable due to space constraints, the opposite `bottom` placement will be used instead.

```jsx noInline
const FormGroup = (props) => (
<Box mb="4x" {...props} />
);

render(() => {
const [colorMode] = useColorMode();
const [colorStyle] = useColorStyle({ colorMode });
const [isFlipModifierEnabled, toggleIsFlipModifierEnabled] = useToggle(true);

return (
<>
<Box mb="4x">
<Text fontSize="lg" lineHeight="lg">
Modifiers
</Text>
</Box>
<FormGroup>
<TextLabel display="inline-flex" alignItems="center">
<Checkbox
checked={isFlipModifierEnabled}
onChange={() => toggleIsFlipModifierEnabled()}
/>
<Space width="2x" />
<Text fontFamily="mono" whiteSpace="nowrap">Enable flip modifier</Text>
</TextLabel>
</FormGroup>
<Divider my="4x" />
<Scrollbar
height={180}
width={180}
overflowY="visible"
border={1}
borderColor={colorStyle.divider}
>
<Flex
alignItems="center"
justifyContent="center"
height={300}
>
<Popover isOpen placement="top">
<PopoverTrigger>
<Text display="inline-block">
Reference
</Text>
</PopoverTrigger>
<PopoverContent
PopperProps={{
modifiers: [
{ // https://popper.js.org/docs/v2/modifiers/flip/
name: 'flip',
enabled: isFlipModifierEnabled,
},
],
}}
>
Popover
</PopoverContent>
</Popover>
</Flex>
</Scrollbar>
</>
);
});
```

## Accessibility

### Keyboard and focus
Expand Down Expand Up @@ -554,7 +630,7 @@ The `Popover` component includes several accessibility features to ensure that i

| Name | Type | Default | Description |
| :--- | :--- | :------ | :---------- |
| PopperComponent | ElementType | Popper | The component used for the popover. |
| PopperComponent | ElementType | Popper | The component used for the popper. |
| PopperProps | object | | Props applied to the Popper component. |
| PopoverArrowComponent | ElementType | PopoverArrow | The component used for the popover arrow. |
| PopoverArrowProps | object | | Props applied to the `PopoverArrow` component. |
Expand Down
76 changes: 73 additions & 3 deletions packages/react-docs/pages/components/tooltip.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -374,11 +374,11 @@ function Example() {

## Commonly Asked Questions

### Preventing tooltip cut-off with `PopperProps`
### Resolving tooltip content cut-off with `PopperProps`

By default, the `Tooltip` component positions the tooltip relative to its parent container. In some cases, the tooltip content might be cut off when it extends outside the container that holds it.

To mitigate this issue, you can pass `PopperProps={{ usePortal: true }}` to `Tooltip` to make it positioned on the document root.
To address this issue, you can pass `PopperProps={{ usePortal: true }}` to `Tooltip` to make it positioned on the document root.

```jsx
<Tooltip
Expand All @@ -389,13 +389,83 @@ To mitigate this issue, you can pass `PopperProps={{ usePortal: true }}` to `Too
</Tooltip>
```

### Automatically adjusting tooltip placement with the `flip` modifier

The `flip` modifier is a useful feature that allows for automatic adjustment of tooltip placement when it is at risk of overflowing the specified boundary. To learn more about utilizing the `flip` modifier, please refer to [Popper.js documentation](https://popper.js.org/docs/v2/modifiers/flip/).

In the following example, the tooltip's placement is initially set to `top`. However, if the placement is not suitable due to space constraints, the opposite `bottom` placement will be used instead.

```jsx noInline
const FormGroup = (props) => (
<Box mb="4x" {...props} />
);

render(() => {
const [colorMode] = useColorMode();
const [colorStyle] = useColorStyle({ colorMode });
const [isFlipModifierEnabled, toggleIsFlipModifierEnabled] = useToggle(true);

return (
<>
<Box mb="4x">
<Text fontSize="lg" lineHeight="lg">
Modifiers
</Text>
</Box>
<FormGroup>
<TextLabel display="inline-flex" alignItems="center">
<Checkbox
checked={isFlipModifierEnabled}
onChange={() => toggleIsFlipModifierEnabled()}
/>
<Space width="2x" />
<Text fontFamily="mono" whiteSpace="nowrap">Enable flip modifier</Text>
</TextLabel>
</FormGroup>
<Divider my="4x" />
<Scrollbar
height={180}
width={180}
overflowY="visible"
border={1}
borderColor={colorStyle.divider}
>
<Flex
alignItems="center"
justifyContent="center"
height={300}
>
<Tooltip
isOpen
placement="top"
label="This is a tooltip"
PopperProps={{
modifiers: [
{ // https://popper.js.org/docs/v2/modifiers/flip/
name: 'flip',
enabled: isFlipModifierEnabled,
},
],
}}
>
<Text display="inline-block">
Reference
</Text>
</Tooltip>
</Flex>
</Scrollbar>
</>
);
});
```

## Props

### Tooltip

| Name | Type | Default | Description |
| :--- | :--- | :------ | :---------- |
| PopperComponent | ElementType | Popper | The component used for the popover. |
| PopperComponent | ElementType | Popper | The component used for the popper. |
| PopperProps | object | | Props applied to the Popper component. |
| TooltipArrowComponent | ElementType | TooltipArrow | The component used for the tooltip arrow. |
| TooltipArrowProps | object | | Props applied to the `TooltipArrow` component. |
Expand Down
32 changes: 17 additions & 15 deletions packages/react/src/popover/PopoverArrow.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ const PopoverArrow = forwardRef((
{
arrowHeight = 8,
arrowWidth = 12,
sx,
...rest
},
ref,
Expand All @@ -16,26 +17,27 @@ const PopoverArrow = forwardRef((
placement,
popoverContentRef,
} = usePopover();
const styleProps = usePopoverArrowStyle({ arrowHeight, arrowWidth, placement });
const colorStyleProps = (() => {
const popoverContentEl = popoverContentRef?.current;
if (isHTMLElement(popoverContentEl)) {
// Compute the background color of the first direct child of the popover content and apply it to the popover arrow
const computedStyle = getComputedStyle(popoverContentEl.firstChild);
return {
color: computedStyle?.backgroundColor,
};
}
return {};
})();
const popoverContentEl = popoverContentRef?.current;
const styleProps = usePopoverArrowStyle({ arrowHeight, arrowWidth });

if (isHTMLElement(popoverContentEl)) {
// Compute the background color of the first direct child of the popover content and apply it to the popover arrow
const computedStyle = getComputedStyle(popoverContentEl.firstChild);
styleProps.color = computedStyle?.backgroundColor;
}

return (
<Box
ref={ref}
role="presentation"
data-popper-arrow // This data attribute is used by the Popper.js library to identify the element to use as the arrow (refer to "popper/Popper.js")
{...styleProps}
{...colorStyleProps}
// The `data-popper-arrow` attribute is utilized by `popper/Popper.js` to designate the element used as the arrow
data-popper-arrow
// The `data-popper-placement` attribute is automatically updated by `popper/Popper.js` to reflect the popper's actual placement
data-popper-placement={placement}
sx={[
styleProps,
...Array.isArray(sx) ? sx : [sx],
]}
{...rest}
/>
);
Expand Down
7 changes: 6 additions & 1 deletion packages/react/src/popover/PopoverContent.js
Original file line number Diff line number Diff line change
Expand Up @@ -209,7 +209,6 @@ const PopoverContent = forwardRef((
anchorEl={popoverTriggerRef.current}
id={popoverId}
isOpen={isOpen}
modifiers={popperModifiers}
placement={placement}
ref={popoverContentRef}
role={role}
Expand All @@ -218,6 +217,12 @@ const PopoverContent = forwardRef((
willUseTransition={true}
zIndex="popover"
{...PopperProps}
modifiers={[
// Default modifiers
...popperModifiers,
// User-defined modifiers
...ensureArray(PopperProps?.modifiers),
]}
>
{({ placement, transition }) => {
const { in: inProp, onEnter, onExited } = { ...transition };
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -117,12 +117,35 @@ exports[`Popover should render correctly 1`] = `
}
.emotion-6 {
color: white;
}
.emotion-6[data-popper-placement^="top"] {
position: absolute;
bottom: 0;
}
.emotion-6[data-popper-placement^="top"]::before {
content: "";
border-top: 8px solid;
border-left: calc(12px/2) solid transparent;
border-right: calc(12px/2) solid transparent;
-webkit-filter: drop-shadow(0 1px 1px rgba(0, 0, 0, 0.08));
filter: drop-shadow(0 1px 1px rgba(0, 0, 0, 0.08));
position: absolute;
bottom: calc(8px * -1);
-webkit-transform: translateX(-50%);
-moz-transform: translateX(-50%);
-ms-transform: translateX(-50%);
transform: translateX(-50%);
}
.emotion-6[data-popper-placement^="bottom"] {
position: absolute;
top: 0;
color: white;
}
.emotion-6::before {
.emotion-6[data-popper-placement^="bottom"]::before {
content: "";
border-bottom: 8px solid;
border-left: calc(12px/2) solid transparent;
Expand All @@ -137,6 +160,46 @@ exports[`Popover should render correctly 1`] = `
transform: translateX(-50%);
}
.emotion-6[data-popper-placement^="left"] {
position: absolute;
right: 0;
}
.emotion-6[data-popper-placement^="left"]::before {
content: "";
border-left: 8px solid;
border-top: calc(12px/2) solid transparent;
border-bottom: calc(12px/2) solid transparent;
-webkit-filter: drop-shadow(1px 0px 1px rgba(0, 0, 0, 0.08));
filter: drop-shadow(1px 0px 1px rgba(0, 0, 0, 0.08));
position: absolute;
right: calc(8px * -1);
-webkit-transform: translateY(-50%);
-moz-transform: translateY(-50%);
-ms-transform: translateY(-50%);
transform: translateY(-50%);
}
.emotion-6[data-popper-placement^="right"] {
position: absolute;
left: 0;
}
.emotion-6[data-popper-placement^="right"]::before {
content: "";
border-right: 8px solid;
border-top: calc(12px/2) solid transparent;
border-bottom: calc(12px/2) solid transparent;
-webkit-filter: drop-shadow(-1px 0px 1px rgba(0, 0, 0, 0.08));
filter: drop-shadow(-1px 0px 1px rgba(0, 0, 0, 0.08));
position: absolute;
left: calc(8px * -1);
-webkit-transform: translateY(-50%);
-moz-transform: translateY(-50%);
-ms-transform: translateY(-50%);
transform: translateY(-50%);
}
<div>
<button
aria-controls="@tonic-ui/react:Popover-1"
Expand Down Expand Up @@ -168,6 +231,7 @@ exports[`Popover should render correctly 1`] = `
<div
class="emotion-6 emotion-1"
data-popper-arrow="true"
data-popper-placement="bottom"
role="presentation"
style="position: absolute; left: 0px; transform: translate(12px, 0px);"
/>
Expand Down
Loading

0 comments on commit 0ce5d4e

Please sign in to comment.