Skip to content

Commit

Permalink
feat(react): add autoAlign feature to popover (#11508)
Browse files Browse the repository at this point in the history
* chore(react): check in progress

* chore: check in progress

* fix(react): check in progress

* chore: check in progress

* feat(react): add autoAlign feature to popover

* chore(react): fix comment typo

* chore(react): remove unnecessary default prop change

* chore(react): remove unnecessary default prop change

* fix(react): resolve lint errors

* chore(react): add comment for explanations

* Update packages/react/src/components/Popover/index.js

Co-authored-by: Josh Black <josh@josh.black>

* fix(react): update ref name

* fix(react): memoize ref value

* chore(react): add experimental language around new prop

Co-authored-by: Josh Black <josh@josh.black>
Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
  • Loading branch information
3 people committed Jun 27, 2022
1 parent f17b814 commit 8aeb459
Show file tree
Hide file tree
Showing 3 changed files with 212 additions and 7 deletions.
3 changes: 3 additions & 0 deletions packages/react/__tests__/__snapshots__/PublicAPI-test.js.snap
Original file line number Diff line number Diff line change
Expand Up @@ -5108,6 +5108,9 @@ Map {
],
"type": "oneOfType",
},
"autoAlign": Object {
"type": "bool",
},
"caret": Object {
"type": "bool",
},
Expand Down
158 changes: 151 additions & 7 deletions packages/react/src/components/Popover/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,22 @@

import cx from 'classnames';
import PropTypes from 'prop-types';
import React from 'react';
import React, { useRef, useState, useMemo } from 'react';
import useIsomorphicEffect from '../../internal/useIsomorphicEffect';
import { useMergedRefs } from '../../internal/useMergedRefs';
import { usePrefix } from '../../internal/usePrefix';

const Popover = React.forwardRef(function Popover(props, ref) {
const PopoverContext = React.createContext({
floating: {
current: null,
},
});

const Popover = React.forwardRef(function Popover(props, forwardRef) {
const {
align = 'bottom',
as: BaseComponent = 'span',
autoAlign = false,
caret = true,
className: customClassName,
children,
Expand All @@ -23,20 +32,148 @@ const Popover = React.forwardRef(function Popover(props, ref) {
...rest
} = props;
const prefix = usePrefix();
const floating = useRef();
const popover = useRef();

const value = useMemo(() => {
return {
floating,
};
}, []);

const ref = useMergedRefs([forwardRef, popover]);
const [autoAligned, setAutoAligned] = useState(false);
const [autoAlignment, setAutoAlignment] = useState(align);
const className = cx({
[`${prefix}--popover-container`]: true,
[`${prefix}--popover--caret`]: caret,
[`${prefix}--popover--drop-shadow`]: dropShadow,
[`${prefix}--popover--high-contrast`]: highContrast,
[`${prefix}--popover--open`]: open,
[`${prefix}--popover--${align}`]: true,
[`${prefix}--popover--${autoAlignment}`]: autoAligned,
[`${prefix}--popover--${align}`]: !autoAligned,
[customClassName]: !!customClassName,
});

useIsomorphicEffect(() => {
if (!autoAlign) {
setAutoAligned(false);
return;
}

if (!floating.current) {
return;
}

if (autoAligned === true) {
return;
}

const rect = floating.current.getBoundingClientRect();

// The conditions, per side, of when the popover is not visible, excluding the popover's internal padding(16)
const conditions = {
left: rect.x < -16,
top: rect.y < -16,
right: rect.x + (rect.width - 16) > document.documentElement.clientWidth,
bottom:
rect.y + (rect.height - 16) > document.documentElement.clientHeight,
};

if (
!conditions.left &&
!conditions.top &&
!conditions.right &&
!conditions.bottom
) {
setAutoAligned(false);
return;
}

const alignments = [
'top',
'top-left',
'right-bottom',
'right',
'right-top',
'bottom-left',
'bottom',
'bottom-right',
'left-top',
'left',
'left-bottom',
'top-right',
];

// Creates the prioritized list of options depending on ideal alignment coming from `align`
const options = [align];
let option =
alignments[(alignments.indexOf(align) + 1) % alignments.length];

while (option) {
if (options.includes(option)) {
break;
}
options.push(option);
option = alignments[(alignments.indexOf(option) + 1) % alignments.length];
}

function isVisible(alignment) {
popover.current.classList.add(`${prefix}--popover--${alignment}`);

const rect = floating.current.getBoundingClientRect();

// Check if popover is not visible to the left of the screen
if (rect.x < -16) {
popover.current.classList.remove(`${prefix}--popover--${alignment}`);
return false;
}

// Check if popover is not visible at the top of the screen
if (rect.y < -16) {
popover.current.classList.remove(`${prefix}--popover--${alignment}`);
return false;
}

// Check if popover is not visible to right of screen
if (rect.x + (rect.width - 16) > document.documentElement.clientWidth) {
popover.current.classList.remove(`${prefix}--popover--${alignment}`);
return false;
}

// Check if popover is not visible to bottom of screen
if (rect.y + (rect.height - 16) > document.documentElement.clientHeight) {
popover.current.classList.remove(`${prefix}--popover--${alignment}`);
return false;
}

popover.current.classList.remove(`${prefix}--popover--${alignment}`);
return true;
}

let alignment = null;

for (let i = 0; i < options.length; i++) {
const option = options[i];

if (isVisible(option)) {
alignment = option;
break;
}
}

if (alignment) {
setAutoAligned(true);
setAutoAlignment(alignment);
}
}, [autoAligned, align, autoAlign, prefix]);

return (
<BaseComponent {...rest} className={className} ref={ref}>
{children}
</BaseComponent>
<PopoverContext.Provider value={value}>
<BaseComponent {...rest} className={className} ref={ref}>
{children}
</BaseComponent>
</PopoverContext.Provider>
);
});

Expand Down Expand Up @@ -74,6 +211,11 @@ Popover.propTypes = {
*/
as: PropTypes.oneOfType([PropTypes.string, PropTypes.elementType]),

/**
* Will auto-align the popover on first render if it is not visible. This prop is currently experimental and is subject to futurue changes.
*/
autoAlign: PropTypes.bool,

/**
* Specify whether a caret should be rendered
*/
Expand Down Expand Up @@ -108,9 +250,11 @@ Popover.propTypes = {

const PopoverContent = React.forwardRef(function PopoverContent(
{ className, children, ...rest },
ref
forwardRef
) {
const prefix = usePrefix();
const { floating } = React.useContext(PopoverContext);
const ref = useMergedRefs([floating, forwardRef]);
return (
<span {...rest} className={`${prefix}--popover`}>
<span className={cx(`${prefix}--popover-content`, className)} ref={ref}>
Expand Down
58 changes: 58 additions & 0 deletions packages/react/src/components/Popover/next/Popover.stories.js
Original file line number Diff line number Diff line change
Expand Up @@ -127,3 +127,61 @@ Playground.story = {
(story) => <div className="mt-10 flex justify-center">{story()}</div>,
],
};

export const AutoAlign = () => {
return (
<div>
<Popover open autoAlign>
<div className="playground-trigger">
<Checkbox />
</div>
<PopoverContent className="p-3">
<p className="popover-title">Available storage</p>
<p className="popover-details">
This server has 150 GB of block storage remaining.
</p>
</PopoverContent>
</Popover>
<div style={{ position: 'absolute', top: 0, right: 0, margin: '3rem' }}>
<Popover open autoAlign>
<div className="playground-trigger">
<Checkbox />
</div>
<PopoverContent className="p-3">
<p className="popover-title">Available storage</p>
<p className="popover-details">
This server has 350 GB of block storage remaining.
</p>
</PopoverContent>
</Popover>
</div>
<div
style={{ position: 'absolute', bottom: 0, right: 0, margin: '3rem' }}>
<Popover open autoAlign>
<div className="playground-trigger">
<Checkbox />
</div>
<PopoverContent className="p-3">
<p className="popover-title">Available storage</p>
<p className="popover-details">
This server has 150 GB of block storage remaining.
</p>
</PopoverContent>
</Popover>
</div>
<div style={{ position: 'absolute', bottom: 0, left: 0, margin: '3rem' }}>
<Popover open autoAlign>
<div className="playground-trigger">
<Checkbox />
</div>
<PopoverContent className="p-3">
<p className="popover-title">Available storage</p>
<p className="popover-details">
This server has 150 GB of block storage remaining.
</p>
</PopoverContent>
</Popover>
</div>
</div>
);
};

0 comments on commit 8aeb459

Please sign in to comment.