Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add Positioner support for Position.LEFT and Position.RIGHT #299

Merged
merged 2 commits into from
Sep 11, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion src/popover/src/Popover.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,9 @@ export default class Popover extends Component {
Position.TOP_RIGHT,
Position.BOTTOM,
Position.BOTTOM_LEFT,
Position.BOTTOM_RIGHT
Position.BOTTOM_RIGHT,
Position.LEFT,
Position.RIGHT
]),

/**
Expand Down
8 changes: 8 additions & 0 deletions src/popover/stories/index.stories.js
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,14 @@ storiesOf('popover', module)
<Button marginRight={20}>TOP_RIGHT</Button>
</Popover>
</Box>
<Box marginTop={40} display="flex" justifyContent="space-between">
<Popover content={<PopoverContent />} position={Position.LEFT}>
<Button marginRight={20}>LEFT</Button>
</Popover>
<Popover content={<PopoverContent />} position={Position.RIGHT}>
<Button marginRight={20}>RIGHT</Button>
</Popover>
</Box>
</Box>
</Box>
))
Expand Down
4 changes: 3 additions & 1 deletion src/positioner/src/Positioner.js
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,9 @@ export default class Positioner extends PureComponent {
Position.TOP_RIGHT,
Position.BOTTOM,
Position.BOTTOM_LEFT,
Position.BOTTOM_RIGHT
Position.BOTTOM_RIGHT,
Position.LEFT,
Position.RIGHT
]).isRequired,

/**
Expand Down
171 changes: 162 additions & 9 deletions src/positioner/src/getPosition.js
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,21 @@ const isAlignedOnTop = position => {
}
}

/**
* Function that returns if position is aligned left or right.
* @param {Position} position
* @return {Boolean}
*/
const isAlignedHorizontal = position => {
switch (position) {
case Position.LEFT:
case Position.RIGHT:
return true
default:
return false
}
}

/**
* Function that returns if a rect fits on bottom.
* @param {Rect} rect
Expand All @@ -83,6 +98,27 @@ const getFitsOnTop = (rect, viewportOffset) => {
return rect.top > viewportOffset
}

/**
* Function that returns if a rect fits on right.
* @param {Rect} rect
* @param {Object} viewport
* @param {Number} viewportOffset
* @return {Boolean}
*/
const getFitsOnRight = (rect, viewport, viewportOffset) => {
return rect.right < viewport.width - viewportOffset
}

/**
* Function that returns if a rect fits on left.
* @param {Rect} rect
* @param {Number} viewportOffset
* @return {Boolean}
*/
const getFitsOnLeft = (rect, viewportOffset) => {
return rect.left > viewportOffset
}

/**
* https://developer.mozilla.org/en-US/docs/Web/CSS/transform-origin
* Function that returns the CSS `tranform-origin` property.
Expand All @@ -93,13 +129,26 @@ const getFitsOnTop = (rect, viewportOffset) => {
* @return {String} transform origin
*/
const getTransformOrigin = ({ rect, position, dimensions, targetCenter }) => {
const center = Math.round(targetCenter - rect.left)
const centerY = Math.round(targetCenter - rect.top)

if (position === Position.LEFT) {
/* Syntax: x-offset | y-offset */
return `${dimensions.width}px ${centerY}px`
}

if (position === Position.RIGHT) {
/* Syntax: x-offset | y-offset */
return `0px ${centerY}px`
}

const centerX = Math.round(targetCenter - rect.left)

if (isAlignedOnTop(position)) {
/* Syntax: x-offset | y-offset */
return `${center}px ${dimensions.height}px `
return `${centerX}px ${dimensions.height}px `
}
/* Syntax: x-offset | y-offset */
return `${center}px 0px `
return `${centerX}px 0px `
}

/**
Expand All @@ -120,8 +169,6 @@ export default function getFittedPosition({
viewport,
viewportOffset = 8
}) {
const targetCenter = targetRect.left + targetRect.width / 2

const { rect, position: finalPosition } = getPosition({
position,
dimensions,
Expand All @@ -144,6 +191,23 @@ export default function getFittedPosition({
rect.right -= delta
}

// Push rect down if overflowing on the top side of the viewport.
if (rect.top < viewportOffset) {
rect.top += Math.ceil(Math.abs(rect.top - viewportOffset))
rect.bottom = Math.ceil(viewportOffset)
}

// Push rect up if overflowing on the bottom side of the viewport.
if (rect.bottom > viewport.height - viewportOffset) {
const delta = Math.ceil(rect.bottom - (viewport.height - viewportOffset))
rect.top -= delta
rect.right -= delta
}

const targetCenter = isAlignedHorizontal(position)
? targetRect.top + targetRect.height / 2
: targetRect.left + targetRect.width / 2

const transformOrigin = getTransformOrigin({
rect,
position: finalPosition,
Expand Down Expand Up @@ -176,6 +240,74 @@ function getPosition({
viewport,
viewportOffset = 8
}) {
const isHorizontal = isAlignedHorizontal(position)

// Handle left and right positions
if (isHorizontal) {
const leftRect = getRect({
position: Position.LEFT,
dimensions,
targetRect,
targetOffset
})

const rightRect = getRect({
position: Position.RIGHT,
dimensions,
targetRect,
targetOffset
})

const fitsOnLeft = getFitsOnLeft(leftRect, viewportOffset)
const fitsOnRight = getFitsOnRight(rightRect, viewport, viewportOffset)

if (position === Position.LEFT) {
if (fitsOnLeft) {
return {
position,
rect: leftRect
}
} else if (fitsOnRight) {
return {
position: Position.RIGHT,
rect: rightRect
}
}
}

if (position === Position.RIGHT) {
if (fitsOnRight) {
return {
position,
rect: rightRect
}
} else if (fitsOnLeft) {
return {
position: Position.LEFT,
rect: leftRect
}
}
}

// Default to using the position with the most space
const spaceRight = Math.abs(
viewport.width - viewportOffset - rightRect.right
)
const spaceLeft = Math.abs(leftRect.left - viewportOffset)

if (spaceRight < spaceLeft) {
return {
position: Position.RIGHT,
rect: rightRect
}
}

return {
position: Position.LEFT,
rect: leftRect
}
}

const positionIsAlignedOnTop = isAlignedOnTop(position)
let topRect
let bottomRect
Expand Down Expand Up @@ -209,16 +341,24 @@ function getPosition({
}

const topRectFitsOnTop = getFitsOnTop(topRect, viewportOffset)

const bottomRectFitsOnBottom = getFitsOnBottom(
bottomRect,
viewport,
viewportOffset
)

if (positionIsAlignedOnTop && topRectFitsOnTop) {
return {
position,
rect: topRect
if (positionIsAlignedOnTop) {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This behaviour makes more sense to me - that it would alter the position to fit below if the popover fits within the viewport... but perhaps I'm missing something.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am not exactly sure how this is different in behavior from what's currently there. An example screenshot would help!

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@jeroenransijn Here are some gifs that demonstrate the difference. With the old logic, the popover will be moved down even if the entire popover could instead be positioned below the button. With the new logic, the popover will check if it can appear entirely below the button. Only if it can't, will it reposition it within its preferred position.

Old:
evergreen-position-old

New:
evergreen-position-new

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks better!

if (topRectFitsOnTop) {
return {
position,
rect: topRect
}
} else if (bottomRectFitsOnBottom) {
return {
position: flipHorizontal(position),
rect: bottomRect
}
}
}

Expand All @@ -240,6 +380,7 @@ function getPosition({
const spaceBottom = Math.abs(
viewport.height - viewportOffset - bottomRect.bottom
)

const spaceTop = Math.abs(topRect.top - viewportOffset)

if (spaceBottom < spaceTop) {
Expand Down Expand Up @@ -268,8 +409,20 @@ function getRect({ position, targetOffset, dimensions, targetRect }) {
const alignedTopY = targetRect.top - dimensions.height - targetOffset
const alignedBottomY = targetRect.bottom + targetOffset
const alignedRightX = targetRect.right - dimensions.width
const alignedLeftRightY =
targetRect.top + targetRect.height / 2 - dimensions.height / 2

switch (position) {
case Position.LEFT:
return makeRect(dimensions, {
left: targetRect.left - dimensions.width - targetOffset,
top: alignedLeftRightY
})
case Position.RIGHT:
return makeRect(dimensions, {
left: targetRect.right + targetOffset,
top: alignedLeftRightY
})
case Position.TOP:
return makeRect(dimensions, {
left: leftRect,
Expand Down
Loading