Skip to content
This repository was archived by the owner on Mar 4, 2020. It is now read-only.

fix(Popup): allow to 'detach' from trigger and RTL adjustments #612

Merged
merged 14 commits into from
Dec 18, 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
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm

## [Unreleased]

### Fixes
- Ensure `Popup` properly flips values of `offset` prop in RTL @kuzhelov ([#612](https://github.com/stardust-ui/react/pull/612))

### Features
- Add `color` prop to `Text` component @Bugaa92 ([#597](https://github.com/stardust-ui/react/pull/597))
- Add `color` prop to `Header` and `HeaderDescription` components @Bugaa92 ([#628](https://github.com/stardust-ui/react/pull/628))
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import React from 'react'
import { Button, Grid, Popup, Alignment, Position } from '@stardust-ui/react'
import { Button, Grid, Popup } from '@stardust-ui/react'

const renderButton = rotateArrowUp => (
<Button
Expand All @@ -11,33 +11,24 @@ const renderButton = rotateArrowUp => (
/>
)

const triggers = [
{ position: 'above', align: 'start', offset: '-100%p', rotateArrowUp: '-45deg' },
{ position: 'above', align: 'end', offset: '100%p', rotateArrowUp: '45deg' },
{ position: 'below', align: 'start', offset: '-100%p', rotateArrowUp: '-135deg' },
{ position: 'below', align: 'end', offset: '100%p', rotateArrowUp: '135deg' },
]

const PopupExamplePosition = () => (
<Grid columns="repeat(2, 80px)" variables={{ padding: '30px', gridGap: '30px' }}>
{triggers.map(({ position, align, offset, rotateArrowUp }) => (
<Popup
align={align as Alignment}
position={position as Position}
offset={offset}
trigger={renderButton(rotateArrowUp)}
content={{
content: (
<p>
The popup is rendered at {position}-{align}
<br />
corner of the trigger.
</p>
),
}}
key={`${position}-${align}`}
/>
))}
<Grid columns="1, 80px" variables={{ padding: '30px' }}>
<Popup
align="start"
position="above"
offset="-100%p"
trigger={renderButton('-45deg')}
content={{
content: (
<p>
The popup is rendered at above-start
<br />
corner of the trigger.
</p>
),
}}
key="above-start"
/>
</Grid>
)

Expand Down
19 changes: 6 additions & 13 deletions src/components/Popup/Popup.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import {
import { ComponentEventHandler, Extendable, ShorthandValue } from '../../../types/utils'

import Ref from '../Ref/Ref'
import computePopupPlacement, { Alignment, Position } from './positioningHelper'
import { getPopupPlacement, applyRtlToOffset, Alignment, Position } from './positioningHelper'

import PopupContent from './PopupContent'

Expand Down Expand Up @@ -257,11 +257,14 @@ export default class Popup extends AutoControlledComponent<Extendable<PopupProps
const { align, position, offset } = this.props
const { target } = this.state

const placement = computePopupPlacement({ align, position, rtl })
const placement = getPopupPlacement({ align, position, rtl })

const popperModifiers = {
// https://popper.js.org/popper-documentation.html#modifiers..offset
...(offset && { offset: { offset: this.applyRtlToOffset(offset, rtl, position) } }),
...(offset && {
offset: { offset: rtl ? applyRtlToOffset(offset, position) : offset },
keepTogether: { enabled: false },
}),
}

return (
Expand Down Expand Up @@ -331,14 +334,4 @@ export default class Popup extends AutoControlledComponent<Extendable<PopupProps
_.invoke(this.props, 'onOpenChange', eventArgs, { ...this.props, ...{ open: newValue } })
}
}

private applyRtlToOffset(offset: string, rtl: boolean, position: Position): string {
if (rtl && (position === 'above' || position === 'below')) {
return offset.trimLeft().startsWith('-')
? offset.trimLeft().replace(/^-\s*/, '')
: `-${offset}`
}

return offset
}
}
29 changes: 28 additions & 1 deletion src/components/Popup/positioningHelper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ const shouldAlignToCenter = (p: Position, a: Alignment) => {
* | after | center | right | left
* | after | bottom | right-end | left-end
*/
export default ({
export const getPopupPlacement = ({
align,
position,
rtl,
Expand All @@ -74,3 +74,30 @@ export default ({

return `${computedPosition}${stringifiedAlignment}` as Placement
}

/////////////////////////////////
// OFFSET VALUES ADJUSTMENT
/////////////////////////////////

const flipPlusMinusSigns = (offset: string): string => {
return offset
.replace(/\-/g, '<plus>')
.replace(/^(\s*)(?=\d)/, '<minus>')
.replace(/\+/g, '<minus>')
.replace(/<plus>/g, '+')
.replace(/<minus>/g, '-')
.trimLeft()
.replace(/^\+/, '')
}
Copy link
Member

Choose a reason for hiding this comment

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

This looks a complicated for me, but we have tests. Possibly, we can to add memoization there.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

yes, the tests were introduced exactly for this sake :) not sure about memoization, as all expressions used are of linear complexity - there shouldn't be any significant gain from it.


export const applyRtlToOffset = (offset: string, position: Position): string => {
if (position === 'above' || position === 'below') {
const [horizontal, vertical] = offset.split(',')
return [flipPlusMinusSigns(horizontal), vertical]
.join(', ')
.replace(/, $/, '')
.trim()
}

return offset
}
50 changes: 48 additions & 2 deletions test/specs/components/Popup/Popup-test.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
import computePopupPlacement, { Position, Alignment } from 'src/components/Popup/positioningHelper'
import {
getPopupPlacement,
applyRtlToOffset,
Position,
Alignment,
} from 'src/components/Popup/positioningHelper'
import { Placement } from 'popper.js'

type PositionTestInput = {
Expand All @@ -16,7 +21,7 @@ describe('Popup', () => {
rtl = false,
}: PositionTestInput) =>
it(`Popup ${position} position is transformed to ${expectedPlacement} Popper's placement`, () => {
const actualPlacement = computePopupPlacement({ align, position, rtl })
const actualPlacement = getPopupPlacement({ align, position, rtl })
expect(actualPlacement).toEqual(expectedPlacement)
})

Expand Down Expand Up @@ -56,4 +61,45 @@ describe('Popup', () => {
testPopupPositionInRtl({ position: 'after', align: 'center', expectedPlacement: 'left' })
testPopupPositionInRtl({ position: 'after', align: 'bottom', expectedPlacement: 'left-end' })
})

describe('Popup offset transformed correctly in RTL', () => {
it("applies transform only for 'above' and 'below' postioning", () => {
const originalOffsetValue = '100%'

expect(applyRtlToOffset(originalOffsetValue, 'above')).not.toBe(originalOffsetValue)
expect(applyRtlToOffset(originalOffsetValue, 'below')).not.toBe(originalOffsetValue)

expect(applyRtlToOffset(originalOffsetValue, 'before')).toBe(originalOffsetValue)
expect(applyRtlToOffset(originalOffsetValue, 'after')).toBe(originalOffsetValue)
})

const expectOffsetTransformResult = (originalOffset, resultOffset) => {
expect(applyRtlToOffset(originalOffset, 'above')).toBe(resultOffset)
}

it('flips sign of simple expressions', () => {
expectOffsetTransformResult('100%', '-100%')
expectOffsetTransformResult(' 2000%p ', '-2000%p')
expectOffsetTransformResult('100 ', '-100')
expectOffsetTransformResult(' - 200vh', '200vh')
Copy link
Member

Choose a reason for hiding this comment

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

Copy link
Contributor Author

Choose a reason for hiding this comment

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

have intentionally selected this approach for the following reasons:

  • the jest.each() is less expressive than explicit name of the check
  • we are not interested in name arg that jest.each() might provide for each case under check
  • even given that this is more verbose, might argue that we will read these lines far more often than write them, so I'd better prefer expressiveness here
  • this approach is consistent with the one that is already taken for the rest of the tests in this file (which is, also, provide more readable tests code)

})

it('flips sign of complex expressions', () => {
expectOffsetTransformResult('100% + 200', '-100% - 200')
expectOffsetTransformResult(' - 2000%p - 400 +800vh ', '2000%p + 400 -800vh')
})

it('transforms only horizontal offset value', () => {
const xOffset = '-100%'
const yOffset = '800vh'

const offsetValue = [xOffset, yOffset].join(',')
const [xOffsetTransformed, yOffsetTransformed] = applyRtlToOffset(offsetValue, 'above').split(
',',
)

expect(xOffsetTransformed.trim()).not.toBe(xOffset)
expect(yOffsetTransformed.trim()).toBe(yOffset)
})
})
})