From 6b73e26e8ba4c0830ec3fe8e42f61e4074b9e9a9 Mon Sep 17 00:00:00 2001 From: bmcmahen Date: Wed, 22 Aug 2018 14:33:08 -0700 Subject: [PATCH 1/2] add Position.LEFT and Position.RIGHT positions to Positioner, Tooltip, and Popover --- src/popover/src/Popover.js | 4 +- src/popover/stories/index.stories.js | 8 + src/positioner/src/Positioner.js | 4 +- src/positioner/src/getPosition.js | 141 ++++++++++++- src/positioner/test/index.js | 126 ++++++++++++ src/positioner/test/snapshots/index.js.md | 210 ++++++++++++++++++++ src/positioner/test/snapshots/index.js.snap | Bin 0 -> 1195 bytes src/tooltip/src/Tooltip.js | 4 +- src/tooltip/stories/index.stories.js | 94 ++++++--- 9 files changed, 560 insertions(+), 31 deletions(-) create mode 100644 src/positioner/test/index.js create mode 100644 src/positioner/test/snapshots/index.js.md create mode 100644 src/positioner/test/snapshots/index.js.snap diff --git a/src/popover/src/Popover.js b/src/popover/src/Popover.js index df061dc64..6790a76f1 100644 --- a/src/popover/src/Popover.js +++ b/src/popover/src/Popover.js @@ -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 ]), /** diff --git a/src/popover/stories/index.stories.js b/src/popover/stories/index.stories.js index d98215579..e0f216f69 100644 --- a/src/popover/stories/index.stories.js +++ b/src/popover/stories/index.stories.js @@ -117,6 +117,14 @@ storiesOf('popover', module) + + } position={Position.LEFT}> + + + } position={Position.RIGHT}> + + + )) diff --git a/src/positioner/src/Positioner.js b/src/positioner/src/Positioner.js index 99db065f7..3c4f486e0 100644 --- a/src/positioner/src/Positioner.js +++ b/src/positioner/src/Positioner.js @@ -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, /** diff --git a/src/positioner/src/getPosition.js b/src/positioner/src/getPosition.js index b1e70c4b0..5511823b1 100644 --- a/src/positioner/src/getPosition.js +++ b/src/positioner/src/getPosition.js @@ -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 @@ -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. @@ -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 ` } /** @@ -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, @@ -144,6 +191,10 @@ export default function getFittedPosition({ rect.right -= delta } + const targetCenter = isAlignedHorizontal(position) + ? targetRect.top + targetRect.height / 2 + : targetRect.left + targetRect.width / 2 + const transformOrigin = getTransformOrigin({ rect, position: finalPosition, @@ -176,6 +227,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 @@ -268,8 +387,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, diff --git a/src/positioner/test/index.js b/src/positioner/test/index.js new file mode 100644 index 000000000..7cd4a8bf0 --- /dev/null +++ b/src/positioner/test/index.js @@ -0,0 +1,126 @@ +import test from 'ava' +import getFittedPosition from '../src/getPosition' +import { Position } from '../../constants' + +const dimensions = overrides => + Object.assign( + {}, + { + height: 100, + width: 100 + }, + overrides + ) + +const targetRect = overrides => + Object.assign( + {}, + { + x: 250, + y: 150, + width: 50, + height: 30, + top: 150, + bottom: 150 - 30, + left: 250, + right: 200 + }, + overrides + ) + +const targetOffset = 6 + +const viewport = overrides => + Object.assign( + {}, + { + height: 250, + width: 850 + }, + overrides + ) + +test('All positions work', t => { + const generatedPositions = Object.values(Position).map(position => + getFittedPosition({ + position, + dimensions: dimensions(), + targetRect: targetRect(), + targetOffset, + viewport: viewport() + }) + ) + t.snapshot(generatedPositions) +}) + +test('Position.LEFT repositions to the right', t => { + t.snapshot( + getFittedPosition({ + position: Position.LEFT, + dimensions: dimensions({ width: 350 }), + targetRect: targetRect(), + targetOffset, + viewport: viewport() + }) + ) +}) + +test('Position.RIGHT repositions to the left', t => { + t.snapshot( + getFittedPosition({ + position: Position.RIGHT, + dimensions: dimensions({ width: 250 }), + targetRect: targetRect({ left: 800, x: 800, right: 850 }), + targetOffset, + viewport: viewport() + }) + ) +}) + +test('Position.LEFT and Position.RIGHT will use the side with the most space', t => { + t.snapshot( + getFittedPosition({ + position: Position.LEFT, + dimensions: dimensions({ width: 250 }), + targetRect: targetRect({ left: 50, x: 50, right: 100 }), + targetOffset, + viewport: viewport({ width: 300 }) + }) + ) +}) + +test('Position.TOP repositions to the bottom', t => { + t.snapshot( + getFittedPosition({ + position: Position.TOP, + dimensions: dimensions({ height: 250 }), + targetRect: targetRect({ top: 20, y: 20 }), + targetOffset, + viewport: viewport() + }) + ) +}) + +test('Position.BOTTOM repositions to the top', t => { + t.snapshot( + getFittedPosition({ + position: Position.BOTTOM, + dimensions: dimensions({ height: 250 }), + targetRect: targetRect({ top: 290, y: 290, bottom: 295, height: 5 }), + targetOffset, + viewport: viewport({ height: 300 }) + }) + ) +}) + +test('It pushes the rect to the right if overflowing on the left side', t => { + t.snapshot( + getFittedPosition({ + position: Position.BOTTOM, + dimensions: dimensions({ width: 250, height: 110 }), + targetRect: targetRect({ left: 10, x: 10, top: 10, y: 10, bottom: 20 }), + targetOffset, + viewport: viewport() + }) + ) +}) diff --git a/src/positioner/test/snapshots/index.js.md b/src/positioner/test/snapshots/index.js.md new file mode 100644 index 000000000..4529da9bf --- /dev/null +++ b/src/positioner/test/snapshots/index.js.md @@ -0,0 +1,210 @@ +# Snapshot report for `src/positioner/test/index.js` + +The actual snapshot is saved in `index.js.snap`. + +Generated by [AVA](https://ava.li). + +## All positions work + +> Snapshot 1 + + [ + { + position: 'top', + rect: { + bottom: 144, + height: 100, + left: 225, + right: 325, + top: 44, + width: 100, + }, + transformOrigin: '50px 100px ', + }, + { + position: 'top-left', + rect: { + bottom: 144, + height: 100, + left: 250, + right: 350, + top: 44, + width: 100, + }, + transformOrigin: '25px 100px ', + }, + { + position: 'top-right', + rect: { + bottom: 144, + height: 100, + left: 100, + right: 200, + top: 44, + width: 100, + }, + transformOrigin: '175px 100px ', + }, + { + position: 'bottom', + rect: { + bottom: 226, + height: 100, + left: 225, + right: 325, + top: 126, + width: 100, + }, + transformOrigin: '50px 0px ', + }, + { + position: 'bottom-left', + rect: { + bottom: 226, + height: 100, + left: 250, + right: 350, + top: 126, + width: 100, + }, + transformOrigin: '25px 0px ', + }, + { + position: 'bottom-right', + rect: { + bottom: 226, + height: 100, + left: 100, + right: 200, + top: 126, + width: 100, + }, + transformOrigin: '175px 0px ', + }, + { + position: 'left', + rect: { + bottom: 215, + height: 100, + left: 144, + right: 244, + top: 115, + width: 100, + }, + transformOrigin: '100px 50px', + }, + { + position: 'right', + rect: { + bottom: 215, + height: 100, + left: 206, + right: 306, + top: 115, + width: 100, + }, + transformOrigin: '0px 50px', + }, + ] + +## It pushes the rect to the right if overflowing on the left side + +> Snapshot 1 + + { + position: 'bottom', + rect: { + bottom: 136, + height: 110, + left: 8, + right: 258, + top: 26, + width: 250, + }, + transformOrigin: '27px 0px ', + } + +## Position.BOTTOM repositions to the top + +> Snapshot 1 + + { + position: 'top', + rect: { + bottom: 284, + height: 250, + left: 225, + right: 325, + top: 34, + width: 100, + }, + transformOrigin: '50px 250px ', + } + +## Position.LEFT and Position.RIGHT will use the side with the most space + +> Snapshot 1 + + { + position: 'right', + rect: { + bottom: 215, + height: 100, + left: 42, + right: 292, + top: 115, + width: 250, + }, + transformOrigin: '0px 50px', + } + +## Position.LEFT repositions to the right + +> Snapshot 1 + + { + position: 'right', + rect: { + bottom: 215, + height: 100, + left: 206, + right: 556, + top: 115, + width: 350, + }, + transformOrigin: '0px 50px', + } + +## Position.RIGHT repositions to the left + +> Snapshot 1 + + { + position: 'left', + rect: { + bottom: 215, + height: 100, + left: 544, + right: 794, + top: 115, + width: 250, + }, + transformOrigin: '250px 50px', + } + +## Position.TOP repositions to the bottom + +> Snapshot 1 + + { + position: 'bottom', + rect: { + bottom: 376, + height: 250, + left: 225, + right: 325, + top: 126, + width: 100, + }, + transformOrigin: '50px 0px ', + } diff --git a/src/positioner/test/snapshots/index.js.snap b/src/positioner/test/snapshots/index.js.snap new file mode 100644 index 0000000000000000000000000000000000000000..4a088a61ab26a2b83447f4e0e30d876b670dbb9f GIT binary patch literal 1195 zcmV;c1XTM$RzV~12L$KJFG-+)Ulfcrgb97dG znIDQ9Lwo`81!dhns1w=P*au;=eo&dB(!HRiQ*=U+OYgEp1Hb0! zx#xeLC+C08$-ViCh%9vQ4a2+*4{P78%j+91{o%U-M#^hf4$;Y)Wu9L~me&_gT+biK z|KbGWt>sRl=#qJzVOH#Hb{`3TaK5>S@z!A%QGfXN){YOa<~ME~?=HKyniy~OhT2b*PYN|4(NYy!hAI zV9yKrWj9R4R~c_@nnNU^a^=M=Om4*c>tG}Bf~}wr>;(J3VNk#Sh1VyfzB8pzyz;W=U zsBony9NfWNbdw5|2(xJo2EzV`KhQJPV2A{Q#BvyWfbpgpjUkL8D4mhe=AN)O5b9nN z@^|>B2e8*v2csp6tChd$rL-bt<(77Dgy$_c>&)w^nPpyC$%`?3vBX}Ti5-iV&Nj2T zB@l@Ox_R2;I@8L0p3~21kL65E^r+s%e4^7&?1(2$%yfxGUuIiurqfStoc^$zbf@ID zDs!D)N>k!v9@3f9n#gEjmJy6{8%%;?E75YW0feo>emAI`5SBso`WT!8*T6W)m2qCm zg4(2}u1BvX&;@pb6W{{)SyWrp)PKmz7Yb1cs0ABAFZjqNsAV*Ye_|46vxeE(WBW|AltH7wf+T?#j5o$kbhnDP5o1y zvLTYHeymY-IEz(Z)~wp2u|Tw|p79)JdGDqVvn=y~5juK_RU&#FIcp_V;mI*+%f*yD}=zLWubWf6m0|pgk#h}tz8}zJgA ( - - {(() => { - document.body.style.margin = '0' - document.body.style.height = '100vh' - })()} - - - Hover to trigger - - - - - Hover to trigger - - - - - Disabled tooltip - - - -)) +storiesOf('tooltip', module) + .add('Tooltip', () => ( + + {(() => { + document.body.style.margin = '0' + document.body.style.height = '100vh' + })()} + + + Hover to trigger + + + + + Hover to trigger + + + + + Disabled tooltip + + + + )) + .add('Positions', () => ( + + {(() => { + document.body.style.margin = '0' + document.body.style.height = '100vh' + })()} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + )) From 37343aaa65df492a1d55777077c2d09a1bae03a7 Mon Sep 17 00:00:00 2001 From: bmcmahen Date: Thu, 23 Aug 2018 13:47:32 -0700 Subject: [PATCH 2/2] alter y axis to keep popover in viewport --- src/positioner/src/getPosition.js | 30 +++++++++++++++++--- src/positioner/test/snapshots/index.js.md | 4 +-- src/positioner/test/snapshots/index.js.snap | Bin 1195 -> 1202 bytes 3 files changed, 28 insertions(+), 6 deletions(-) diff --git a/src/positioner/src/getPosition.js b/src/positioner/src/getPosition.js index 5511823b1..7fc9f6588 100644 --- a/src/positioner/src/getPosition.js +++ b/src/positioner/src/getPosition.js @@ -191,6 +191,19 @@ 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 @@ -328,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) { + if (topRectFitsOnTop) { + return { + position, + rect: topRect + } + } else if (bottomRectFitsOnBottom) { + return { + position: flipHorizontal(position), + rect: bottomRect + } } } @@ -359,6 +380,7 @@ function getPosition({ const spaceBottom = Math.abs( viewport.height - viewportOffset - bottomRect.bottom ) + const spaceTop = Math.abs(topRect.top - viewportOffset) if (spaceBottom < spaceTop) { diff --git a/src/positioner/test/snapshots/index.js.md b/src/positioner/test/snapshots/index.js.md index 4529da9bf..daca38678 100644 --- a/src/positioner/test/snapshots/index.js.md +++ b/src/positioner/test/snapshots/index.js.md @@ -202,8 +202,8 @@ Generated by [AVA](https://ava.li). bottom: 376, height: 250, left: 225, - right: 325, - top: 126, + right: 191, + top: -8, width: 100, }, transformOrigin: '50px 0px ', diff --git a/src/positioner/test/snapshots/index.js.snap b/src/positioner/test/snapshots/index.js.snap index 4a088a61ab26a2b83447f4e0e30d876b670dbb9f..8b1ccd6487159514aa0be2c4cc4ea0f27a01cbd0 100644 GIT binary patch literal 1202 zcmV;j1Wo%vRzVm_5^IgFdvHu00000000x! zm~CiNR~W~ibMtzWv`Jd4AT5?!rH)o>uN37Bf<=+-g! z@}X>Fh%X?%Fj@73Z-}D!L2%X&Dk>`73o33y{b0w0Nr%?|JxQD9?-4}wkbr*>h9fne(Cmw-wPHzsOUStit*Ng0-_zi9Gf-!#qnU* z)cn$0rlM<%w>C^8l2EzwVHPIWO#=mDRBufZNLg<0~LsU{TK*v;5j)!rcz&hwH@%crq^$U$)f?e?%gj3Sda5RwS61?33|}mn zq^7P#uLjToJ_iTE1@Nn=wy3H9k(CDuQ8B0n>p?g8)+VTBHT6;SIt6Zlr@(C|ng{Ca zg4(X8ZbPq5@D=z0Tm%omsHo0SQx`gjNL>BEuT{C=Vy9V)aF*Wyap^r#8oN~EXWwDMS>-_xAGKT* zZuYlCe2J8=v+5DesyDouRi~UwD*BQ*ZYGPqpN$J-TNbOWx$?^@4!cN`ZMtz z7zRDYFn~12L$KJFG-+)Ulfcrgb97dG znIDQ9Lwo`81!dhns1w=P*au;=eo&dB(!HRiQ*=U+OYgEp1Hb0! zx#xeLC+C08$-ViCh%9vQ4a2+*4{P78%j+91{o%U-M#^hf4$;Y)Wu9L~me&_gT+biK z|KbGWt>sRl=#qJzVOH#Hb{`3TaK5>S@z!A%QGfXN){YOa<~ME~?=HKyniy~OhT2b*PYN|4(NYy!hAI zV9yKrWj9R4R~c_@nnNU^a^=M=Om4*c>tG}Bf~}wr>;(J3VNk#Sh1VyfzB8pzyz;W=U zsBony9NfWNbdw5|2(xJo2EzV`KhQJPV2A{Q#BvyWfbpgpjUkL8D4mhe=AN)O5b9nN z@^|>B2e8*v2csp6tChd$rL-bt<(77Dgy$_c>&)w^nPpyC$%`?3vBX}Ti5-iV&Nj2T zB@l@Ox_R2;I@8L0p3~21kL65E^r+s%e4^7&?1(2$%yfxGUuIiurqfStoc^$zbf@ID zDs!D)N>k!v9@3f9n#gEjmJy6{8%%;?E75YW0feo>emAI`5SBso`WT!8*T6W)m2qCm zg4(2}u1BvX&;@pb6W{{)SyWrp)PKmz7Yb1cs0ABAFZjqNsAV*Ye_|46vxeE(WBW|AltH7wf+T?#j5o$kbhnDP5o1y zvLTYHeymY-IEz(Z)~wp2u|Tw|p79)JdGDqVvn=y~5juK_RU&#FIcp_V;mI*+%f*yD}=zLWubWf6m0|pgk#h}tz8}zJgA