Skip to content

Commit

Permalink
Merge 901c29d into 44515d8
Browse files Browse the repository at this point in the history
  • Loading branch information
chenesan committed Dec 10, 2020
2 parents 44515d8 + 901c29d commit c1a0715
Show file tree
Hide file tree
Showing 5 changed files with 60 additions and 18 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Expand Up @@ -21,6 +21,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
- [Core] Upgrade `eslint-config-ichef` packages to `v8.0.0`. (#290)
- [Core] Use node 12 in travis CI. (#290)
- [Core] Add `titleRightArea` on `<Section>` / `<List>`. (#297)
- [Core] Fix popover placement when there is no enough space on top or bottom of content. (#303)

### Added
- [Core] Add `muted` prop on following component: (#278)
Expand Down
17 changes: 16 additions & 1 deletion packages/core/src/Popover.js
Expand Up @@ -24,12 +24,15 @@ export const BEM = {
container: ROOT_BEM.element('container'),
};

const POPOVER_PADDING = 24;

function Popover({
onClick,
// from anchored()
placement,
arrowStyle,
nodeRef,
remainingSpace,
// from closable()
onInsideClick,
// React props
Expand All @@ -39,6 +42,13 @@ function Popover({
}) {
const bemClass = BEM.root.modifier(placement);
const rootClassName = classNames(bemClass.toString(), className);
/**
* The `remainingSpace` is the space for whole popover.
* What we want here is to always show keep `remainingSpace === popoverHeight`
* The `maxHeight` is for `BEM.container`, which doesn't include root class padding.
* So we need to minus POPOVER_PADDING here.
*/
const maxHeight = remainingSpace ? remainingSpace - POPOVER_PADDING : undefined;

const handleWrapperClick = (event) => {
onInsideClick(event);
Expand All @@ -55,7 +65,10 @@ function Popover({
{...otherProps}
>
<span className={BEM.arrow} style={arrowStyle} />
<div className={BEM.container}>
<div
className={BEM.container}
style={{ maxHeight }}
>
{children}
</div>
</div>
Expand All @@ -68,6 +81,7 @@ Popover.propTypes = {
placement: anchoredPropTypes.placement,
arrowStyle: anchoredPropTypes.arrowStyle,
nodeRef: anchoredPropTypes.nodeRef,
remainingSpace: anchoredPropTypes.remainingSpace,
onInsideClick: PropTypes.func.isRequired,
};

Expand All @@ -76,6 +90,7 @@ Popover.defaultProps = {
placement: ANCHORED_PLACEMENT.BOTTOM,
arrowStyle: {},
nodeRef: undefined,
remainingSpace: undefined,
};

export { Popover as PurePopover };
Expand Down
@@ -1,7 +1,7 @@
import documentOffset from 'document-offset';

import getPositionState, {
getPlacement,
getPlacementAndRemainingSpace,
getTopPosition,
getLeftPositionSet,
PLACEMENT,
Expand Down Expand Up @@ -31,23 +31,28 @@ jest.mock('document-offset', () => (
describe('getPlacement()', () => {
const { TOP, BOTTOM } = PLACEMENT;
const runTest = it.each`
expected | defaultVal | situation | anchorTop | anchorHeight | selfHeight
${TOP} | ${TOP} | ${'enough'} | ${120} | ${30} | ${100}
${BOTTOM} | ${TOP} | ${'not enough'} | ${90} | ${30} | ${100}
${TOP} | ${BOTTOM} | ${'not enough'} | ${600} | ${100} | ${100}
${BOTTOM} | ${BOTTOM} | ${'enough'} | ${300} | ${100} | ${100}
expected | defaultVal | situation | anchorTop | anchorHeight | selfHeight | remainingSpace
${TOP} | ${TOP} | ${'enough'} | ${120} | ${30} | ${100} | ${120}
${BOTTOM} | ${TOP} | ${'not enough for top'} | ${90} | ${30} | ${100} | ${648}
${TOP} | ${BOTTOM} | ${'not enough for bottom'} | ${600} | ${100} | ${100} | ${600}
${BOTTOM} | ${BOTTOM} | ${'enough'} | ${300} | ${100} | ${100} | ${368}
${BOTTOM} | ${BOTTOM} | ${'not enough for both, but bottom is larger'} | ${300} | ${100} | ${400} | ${368}
${TOP} | ${BOTTOM} | ${'not enough for both, but top is larger'} | ${450} | ${100} | ${500} | ${450}
`;

runTest(
'returns $expected when default is $defaultVal, and there is $situation space',
({ expected, defaultVal, anchorTop, anchorHeight, selfHeight }) => {
const result = getPlacement(
'returns $expected when default is $defaultVal, and the space is $situation',
({
expected, defaultVal, anchorTop, anchorHeight, selfHeight, remainingSpace,
}) => {
const result = getPlacementAndRemainingSpace(
defaultVal,
anchorTop,
anchorHeight,
selfHeight,
);
expect(result).toBe(expected);
expect(result.placement).toBe(expected);
expect(result.remainingSpace).toBe(remainingSpace);
}
);
});
Expand Down
32 changes: 25 additions & 7 deletions packages/core/src/mixins/anchored/getPositionState.js
Expand Up @@ -31,21 +31,37 @@ export const PLACEMENT = { TOP, BOTTOM };
* @param {number} anchorRectTop
* @param {number} anchorHeight
* @param {number} selfHeight
* @returns {{ placement: Placement, remainingSpace: number }}
*/
export function getPlacement(defaultPlacement, anchorRectTop, anchorHeight, selfHeight) {
export function getPlacementAndRemainingSpace(
defaultPlacement,
anchorRectTop,
anchorHeight,
selfHeight
) {
const hasSpaceToPlaceSelfAbove = anchorRectTop >= selfHeight;
const hasSpaceToPlaceSelfBelow = (
(anchorRectTop + anchorHeight + selfHeight) <= window.innerHeight
);

const topSpace = anchorRectTop;
const bottomSpace = window.innerHeight - anchorRectTop - anchorHeight;
if (!hasSpaceToPlaceSelfBelow && !hasSpaceToPlaceSelfAbove) {
return {
placement: topSpace > bottomSpace ? TOP : BOTTOM,
remainingSpace: topSpace > bottomSpace ? topSpace : bottomSpace,
};
}
if (defaultPlacement === TOP && !hasSpaceToPlaceSelfAbove) {
return BOTTOM;
return { placement: BOTTOM, remainingSpace: bottomSpace };
}
if (defaultPlacement === BOTTOM && !hasSpaceToPlaceSelfBelow) {
return TOP;
return { placement: TOP, remainingSpace: topSpace };
}

return defaultPlacement;
return {
placement: defaultPlacement,
remainingSpace: defaultPlacement === TOP ? topSpace : bottomSpace,
};
}

/**
Expand All @@ -60,7 +76,8 @@ export function getTopPosition(placement, anchorOffsetTop, anchorHeight, selfHei
let positionTop = 0;

if (placement === TOP) {
positionTop = anchorOffsetTop - selfHeight;
// Make sure user can see whole wrapped component when placement is TOP.
positionTop = Math.max(anchorOffsetTop - selfHeight, 0);
} else {
positionTop = anchorOffsetTop + anchorHeight;
}
Expand Down Expand Up @@ -184,7 +201,7 @@ const getPositionState = (defaultPlacement, edgePadding) => (anchorNode, selfNod
// Determine position
// -------------------------------------

const placement = getPlacement(
const { placement, remainingSpace } = getPlacementAndRemainingSpace(
defaultPlacement,
anchorRect.top,
anchorRect.height,
Expand All @@ -208,6 +225,7 @@ const getPositionState = (defaultPlacement, edgePadding) => (anchorNode, selfNod

return {
placement,
remainingSpace,
position: {
top: selfTop,
left: selfLeft,
Expand Down
3 changes: 3 additions & 0 deletions packages/core/src/mixins/anchored/index.js
Expand Up @@ -15,6 +15,7 @@ export const anchoredPropTypes = {
placement: PropTypes.oneOf(Object.values(PLACEMENT)),
arrowStyle: PropTypes.objectOf(PropTypes.number),
nodeRef: PropTypes.func,
remainingSpace: PropTypes.number.isRequired,
};

function filterDOMNode(node) {
Expand Down Expand Up @@ -127,6 +128,7 @@ const anchored = ({
placement,
position,
arrowPosition,
remainingSpace,
} = this.getPositions(
filterDOMNode(anchor),
filterDOMNode(selfNode),
Expand All @@ -142,6 +144,7 @@ const anchored = ({
<WrappedComponent
{...otherProps}
placement={placement}
remainingSpace={remainingSpace}
arrowStyle={arrowPosition}
style={mergedStyle}
nodeRef={this.setSelfNode}
Expand Down

0 comments on commit c1a0715

Please sign in to comment.