diff --git a/docs/src/examples/components/Dropdown/Variations/DropdownExampleOffset.shorthand.tsx b/docs/src/examples/components/Dropdown/Variations/DropdownExampleOffset.shorthand.tsx
new file mode 100644
index 0000000000..8c0ab10111
--- /dev/null
+++ b/docs/src/examples/components/Dropdown/Variations/DropdownExampleOffset.shorthand.tsx
@@ -0,0 +1,18 @@
+import * as React from 'react'
+import { Grid, Dropdown } from '@stardust-ui/react'
+
+const inputItems = ['Bruce Wayne', 'Natasha Romanoff', 'Steven Strange', 'Alfred Pennyworth']
+
+const DropdownExamplePosition = () => (
+
+
+
+)
+
+export default DropdownExamplePosition
diff --git a/docs/src/examples/components/Dropdown/Variations/DropdownExamplePosition.shorthand.tsx b/docs/src/examples/components/Dropdown/Variations/DropdownExamplePosition.shorthand.tsx
new file mode 100644
index 0000000000..6c1c103ca0
--- /dev/null
+++ b/docs/src/examples/components/Dropdown/Variations/DropdownExamplePosition.shorthand.tsx
@@ -0,0 +1,38 @@
+import * as React from 'react'
+import { Grid, Dropdown } from '@stardust-ui/react'
+
+const inputItems = ['Bruce Wayne', 'Natasha Romanoff', 'Steven Strange', 'Alfred Pennyworth']
+
+const DropdownArrowExample = props => {
+ const { position, align } = props
+
+ return (
+
+ )
+}
+
+const triggers = [
+ { position: 'above', align: 'start' },
+ { position: 'above', align: 'end' },
+ { position: 'below', align: 'start' },
+ { position: 'below', align: 'end' },
+ { position: 'before', align: 'top' },
+ { position: 'before', align: 'bottom' },
+ { position: 'after', align: 'top' },
+ { position: 'after', align: 'bottom' },
+]
+
+const DropdownExamplePosition = () => (
+
+ {triggers.map(({ position, align }) => (
+
+ ))}
+
+)
+
+export default DropdownExamplePosition
diff --git a/docs/src/examples/components/Dropdown/Variations/index.tsx b/docs/src/examples/components/Dropdown/Variations/index.tsx
index f9978ec1b5..52a97965ec 100644
--- a/docs/src/examples/components/Dropdown/Variations/index.tsx
+++ b/docs/src/examples/components/Dropdown/Variations/index.tsx
@@ -19,6 +19,16 @@ const Variations = () => (
description="A multiple search dropdown that uses French to provide information and accessibility."
examplePath="components/Dropdown/Variations/DropdownExampleSearchMultipleFrenchLanguage"
/>
+
+
)
diff --git a/packages/react-component-ref/src/Ref.tsx b/packages/react-component-ref/src/Ref.tsx
index ee2face475..27afbacfca 100644
--- a/packages/react-component-ref/src/Ref.tsx
+++ b/packages/react-component-ref/src/Ref.tsx
@@ -14,7 +14,7 @@ export interface RefProps {
*
* @param {HTMLElement} node - Referred node.
*/
- innerRef: React.Ref
+ innerRef: React.Ref
}
const Ref: React.FunctionComponent = props => {
diff --git a/packages/react/src/components/Dropdown/Dropdown.tsx b/packages/react/src/components/Dropdown/Dropdown.tsx
index 17783a0cd2..f56127dbab 100644
--- a/packages/react/src/components/Dropdown/Dropdown.tsx
+++ b/packages/react/src/components/Dropdown/Dropdown.tsx
@@ -30,7 +30,7 @@ import {
commonPropTypes,
UIComponentProps,
} from '../../lib'
-import List from '../List/List'
+import List, { ListProps } from '../List/List'
import DropdownItem, { DropdownItemProps } from './DropdownItem'
import DropdownSelectedItem, { DropdownSelectedItemProps } from './DropdownSelectedItem'
import DropdownSearchInput, { DropdownSearchInputProps } from './DropdownSearchInput'
@@ -39,6 +39,13 @@ import { screenReaderContainerStyles } from '../../lib/accessibility/Styles/acce
import ListItem from '../List/ListItem'
import Icon, { IconProps } from '../Icon/Icon'
import Portal from '../Portal/Portal'
+import {
+ ALIGNMENTS,
+ POSITIONS,
+ Positioner,
+ PositionCommonProps,
+ UpdatableComponent,
+} from '../../lib/positioner'
export interface DropdownSlotClassNames {
clearIndicator: string
@@ -52,7 +59,9 @@ export interface DropdownSlotClassNames {
triggerButton: string
}
-export interface DropdownProps extends UIComponentProps {
+export interface DropdownProps
+ extends UIComponentProps,
+ PositionCommonProps {
/** The index of the currently active selected item, if dropdown has a multiple selection. */
activeSelectedIndex?: number
@@ -236,6 +245,7 @@ class Dropdown extends AutoControlledComponent, Dropdo
content: false,
}),
activeSelectedIndex: PropTypes.number,
+ align: PropTypes.oneOf(_.without(ALIGNMENTS)),
clearable: PropTypes.bool,
clearIndicator: customPropTypes.itemShorthand,
defaultActiveSelectedIndex: PropTypes.number,
@@ -264,6 +274,7 @@ class Dropdown extends AutoControlledComponent, Dropdo
onSelectedChange: PropTypes.func,
open: PropTypes.bool,
placeholder: PropTypes.string,
+ position: PropTypes.oneOf(POSITIONS),
renderItem: PropTypes.func,
renderSelectedItem: PropTypes.func,
search: PropTypes.oneOfType([PropTypes.bool, PropTypes.func]),
@@ -278,6 +289,7 @@ class Dropdown extends AutoControlledComponent, Dropdo
}
static defaultProps: DropdownProps = {
+ align: 'start',
as: 'div',
clearIndicator: 'stardust-close',
itemToString: item => {
@@ -288,6 +300,7 @@ class Dropdown extends AutoControlledComponent, Dropdo
// targets DropdownItem shorthand objects
return (item as any).header || String(item)
},
+ position: 'below',
toggleIndicator: {},
triggerButton: {},
}
@@ -421,7 +434,7 @@ class Dropdown extends AutoControlledComponent, Dropdo
},
}),
})}
- {this.renderItemsList(
+ {this.preparePropsAndRenderItemsList(
styles,
variables,
highlightedIndex,
@@ -430,6 +443,7 @@ class Dropdown extends AutoControlledComponent, Dropdo
getMenuProps,
getItemProps,
getInputProps,
+ rtl,
)}
@@ -518,7 +532,7 @@ class Dropdown extends AutoControlledComponent, Dropdo
})
}
- private renderItemsList(
+ private preparePropsAndRenderItemsList(
styles: ComponentSlotStylesInput,
variables: ComponentVariablesInput,
highlightedIndex: number,
@@ -527,6 +541,7 @@ class Dropdown extends AutoControlledComponent, Dropdo
getMenuProps: (options?: GetMenuPropsOptions, otherOptions?: GetPropsCommonOptions) => any,
getItemProps: (options: GetItemPropsOptions) => any,
getInputProps: (options?: GetInputPropsOptions) => any,
+ rtl: boolean,
) {
const { search } = this.props
const { open } = this.state
@@ -559,20 +574,47 @@ class Dropdown extends AutoControlledComponent, Dropdo
handleRef(innerRef, listElement)
}}
>
-
+ {this.renderItemsList(
+ {
+ className: Dropdown.slotClassNames.itemsList,
+ ...accessibilityMenuProps,
+ styles: styles.list,
+ tabIndex: search ? undefined : -1, // needs to be focused when trigger button is activated.
+ 'aria-hidden': !open,
+ onFocus: this.handleTriggerButtonOrListFocus,
+ onBlur: this.handleListBlur,
+ items: open ? this.renderItems(styles, variables, getItemProps, highlightedIndex) : [],
+ },
+ rtl,
+ )}
)
}
+ private renderItemsList(listProps: ListProps, rtl: boolean): JSX.Element {
+ const { align, position, offset } = this.props
+
+ return (
+ (
+
+ )}
+ />
+ )
+ }
+
private renderItems(
styles: ComponentSlotStylesInput,
variables: ComponentVariablesInput,
diff --git a/packages/react/src/components/Popup/Popup.tsx b/packages/react/src/components/Popup/Popup.tsx
index e8c006e1db..049aa25b66 100644
--- a/packages/react/src/components/Popup/Popup.tsx
+++ b/packages/react/src/components/Popup/Popup.tsx
@@ -7,7 +7,7 @@ import * as ReactDOM from 'react-dom'
import * as PropTypes from 'prop-types'
import * as keyboardKey from 'keyboard-key'
import * as _ from 'lodash'
-import { Popper, PopperChildrenProps } from 'react-popper'
+import { PopperChildrenProps } from 'react-popper'
import {
applyAccessibilityKeyHandlers,
@@ -24,12 +24,8 @@ import {
setWhatInputSource,
} from '../../lib'
import { ComponentEventHandler, ReactProps, ShorthandValue } from '../../types'
-
-import { getPopupPlacement, applyRtlToOffset, Alignment, Position } from './positioningHelper'
-import createPopperReferenceProxy from './createPopperReferenceProxy'
-
+import { ALIGNMENTS, POSITIONS, Positioner, PositionCommonProps } from '../../lib/positioner'
import PopupContent from './PopupContent'
-
import { popupBehavior } from '../../lib/accessibility'
import {
AutoFocusZone,
@@ -44,9 +40,6 @@ import {
AccessibilityBehavior,
} from '../../lib/accessibility/types'
-const POSITIONS: Position[] = ['above', 'below', 'before', 'after']
-const ALIGNMENTS: Alignment[] = ['top', 'bottom', 'start', 'end', 'center']
-
export type PopupEvents = 'click' | 'hover' | 'focus'
export type RestrictedClickEvents = 'click' | 'focus'
export type RestrictedHoverEvents = 'hover' | 'focus'
@@ -59,7 +52,8 @@ export interface PopupSlotClassNames {
export interface PopupProps
extends StyledComponentProps,
ChildrenComponentProps,
- ContentComponentProps {
+ ContentComponentProps,
+ PositionCommonProps {
/**
* Accessibility behavior if overridden by the user.
* @default popupBehavior
@@ -67,9 +61,6 @@ export interface PopupProps
* */
accessibility?: Accessibility
- /** Alignment for the popup. */
- align?: Alignment
-
/** Additional CSS class name(s) to apply. */
className?: string
@@ -88,15 +79,6 @@ export interface PopupProps
/** Delay in ms for the mouse leave event, before the popup will be closed. */
mouseLeaveDelay?: number
- /** Offset value to apply to rendered popup. Accepts the following units:
- * - px or unit-less, interpreted as pixels
- * - %, percentage relative to the length of the trigger element
- * - %p, percentage relative to the length of the popup element
- * - vw, CSS viewport width unit
- * - vh, CSS viewport height unit
- */
- offset?: string
-
/** Events triggering the popup. */
on?: PopupEvents | PopupEventsArray
@@ -113,13 +95,6 @@ export interface PopupProps
/** A popup can show a pointer to trigger. */
pointing?: boolean
- /**
- * Position for the popup. Position has higher priority than align. If position is vertical ('above' | 'below')
- * and align is also vertical ('top' | 'bottom') or if both position and align are horizontal ('before' | 'after'
- * and 'start' | 'end' respectively), then provided value for 'align' will be ignored and 'center' will be used instead.
- */
- position?: Position
-
/**
* Function to render popup content.
* @param {Function} updatePosition - function to request popup position update.
@@ -140,7 +115,6 @@ export interface PopupProps
export interface PopupState {
open: boolean
- target: HTMLElement
}
/**
@@ -409,27 +383,16 @@ export default class Popup extends AutoControlledComponent
)
}
diff --git a/packages/react/src/index.ts b/packages/react/src/index.ts
index 6bc2116a1c..51e34bcf23 100644
--- a/packages/react/src/index.ts
+++ b/packages/react/src/index.ts
@@ -123,7 +123,7 @@ export {
PopupEventsArray,
} from './components/Popup/Popup'
export { default as PopupContent, PopupContentProps } from './components/Popup/PopupContent'
-export { Placement, Alignment, Position } from './components/Popup/positioningHelper'
+export { Alignment, Position } from './lib/positioner'
export {
default as Portal,
diff --git a/packages/react/src/lib/positioner/Positioner.tsx b/packages/react/src/lib/positioner/Positioner.tsx
new file mode 100644
index 0000000000..f0577bf4e1
--- /dev/null
+++ b/packages/react/src/lib/positioner/Positioner.tsx
@@ -0,0 +1,64 @@
+import * as React from 'react'
+import { Popper, PopperChildrenProps, PopperProps } from 'react-popper'
+import { Modifiers } from 'popper.js'
+
+import { Alignment, Position } from './index'
+import { getPlacement, applyRtlToOffset } from './positioningHelper'
+import createPopperReferenceProxy from './createPopperReferenceProxy'
+
+export interface PositionCommonProps {
+ /** Alignment for the component. */
+ align?: Alignment
+
+ /** Offset value to apply to rendered component. Accepts the following units:
+ * - px or unit-less, interpreted as pixels
+ * - %, percentage relative to the length of the trigger element
+ * - %p, percentage relative to the length of the component element
+ * - vw, CSS viewport width unit
+ * - vh, CSS viewport height unit
+ */
+ offset?: string
+
+ /**
+ * Position for the component. Position has higher priority than align. If position is vertical ('above' | 'below')
+ * and align is also vertical ('top' | 'bottom') or if both position and align are horizontal ('before' | 'after'
+ * and 'start' | 'end' respectively), then provided value for 'align' will be ignored and 'center' will be used instead.
+ */
+ position?: Position
+}
+
+interface PositionerProps extends PopperProps, PositionCommonProps {
+ /**
+ * Content for children using render props API
+ */
+ children: (props: PopperChildrenProps) => React.ReactNode
+
+ /**
+ * rtl attribute for the component
+ */
+ rtl?: boolean
+
+ target?: HTMLElement | React.RefObject
+}
+
+const Positioner: React.FunctionComponent = props => {
+ const { align, children, position, offset, rtl, target, ...rest } = props
+ // https://popper.js.org/popper-documentation.html#modifiers..offset
+ const popperModifiers: Modifiers = offset && {
+ offset: { offset: rtl ? applyRtlToOffset(offset, position) : offset },
+ keepTogether: { enabled: false },
+ }
+
+ return (
+
+ )
+}
+
+export default Positioner
diff --git a/packages/react/src/lib/positioner/UpdatableComponent.tsx b/packages/react/src/lib/positioner/UpdatableComponent.tsx
new file mode 100644
index 0000000000..c53791c9f6
--- /dev/null
+++ b/packages/react/src/lib/positioner/UpdatableComponent.tsx
@@ -0,0 +1,43 @@
+import * as React from 'react'
+import { Ref } from '@stardust-ui/react-component-ref'
+
+import { Extendable } from '../../types'
+
+interface UpdatableListProps {
+ /**
+ * Component that will be rendered.
+ */
+ Component: React.ComponentType
+
+ /**
+ * Called when a child component will be mounted or updated.
+ *
+ * @param {HTMLElement} node - Referred node.
+ */
+ innerRef?: React.Ref
+
+ /**
+ * Function that will trigger the rerender.
+ */
+ scheduleUpdate: Function
+
+ /**
+ * Array of conditions to be met in order to trigger a subsequent render.
+ */
+ updateDependencies: any[]
+}
+
+const UpdatableComponent: React.FunctionComponent> = props => {
+ const { Component, innerRef, scheduleUpdate, updateDependencies, ...rest } = props
+
+ React.useEffect(() => scheduleUpdate && scheduleUpdate(), updateDependencies)
+
+ if (!innerRef) return
+ return (
+ [
+
+ ]
+ )
+}
+
+export default UpdatableComponent
diff --git a/packages/react/src/components/Popup/createPopperReferenceProxy.ts b/packages/react/src/lib/positioner/createPopperReferenceProxy.ts
similarity index 92%
rename from packages/react/src/components/Popup/createPopperReferenceProxy.ts
rename to packages/react/src/lib/positioner/createPopperReferenceProxy.ts
index 59bd7249c7..334d129673 100644
--- a/packages/react/src/components/Popup/createPopperReferenceProxy.ts
+++ b/packages/react/src/lib/positioner/createPopperReferenceProxy.ts
@@ -4,11 +4,7 @@ import * as React from 'react'
import * as PopperJS from 'popper.js'
class ReferenceProxy implements PopperJS.ReferenceObject {
- ref: React.RefObject
-
- constructor(refObject) {
- this.ref = refObject
- }
+ constructor(private ref: React.RefObject) {}
getBoundingClientRect() {
return _.invoke(this.ref.current, 'getBoundingClientRect', {})
diff --git a/packages/react/src/lib/positioner/index.ts b/packages/react/src/lib/positioner/index.ts
new file mode 100644
index 0000000000..a538bcac98
--- /dev/null
+++ b/packages/react/src/lib/positioner/index.ts
@@ -0,0 +1,7 @@
+export type Position = 'above' | 'below' | 'before' | 'after'
+export type Alignment = 'top' | 'bottom' | 'start' | 'end' | 'center'
+export const POSITIONS: Position[] = ['above', 'below', 'before', 'after']
+export const ALIGNMENTS: Alignment[] = ['top', 'bottom', 'start', 'end', 'center']
+
+export { default as Positioner, PositionCommonProps } from './Positioner'
+export { default as UpdatableComponent } from './UpdatableComponent'
diff --git a/packages/react/src/components/Popup/positioningHelper.ts b/packages/react/src/lib/positioner/positioningHelper.ts
similarity index 93%
rename from packages/react/src/components/Popup/positioningHelper.ts
rename to packages/react/src/lib/positioner/positioningHelper.ts
index 92e74d367a..8a32f642e6 100644
--- a/packages/react/src/components/Popup/positioningHelper.ts
+++ b/packages/react/src/lib/positioner/positioningHelper.ts
@@ -1,8 +1,6 @@
-export { Placement } from 'popper.js'
import { Placement } from 'popper.js'
-export type Position = 'above' | 'below' | 'before' | 'after'
-export type Alignment = 'top' | 'bottom' | 'start' | 'end' | 'center'
+import { Alignment, Position } from './index'
enum PlacementParts {
top = 'top',
@@ -56,7 +54,7 @@ const shouldAlignToCenter = (p: Position, a: Alignment) => {
* | after | center | right | left
* | after | bottom | right-end | left-end
*/
-export const getPopupPlacement = ({
+export const getPlacement = ({
align,
position,
rtl,
diff --git a/packages/react/src/themes/teams/components/Dropdown/dropdownStyles.ts b/packages/react/src/themes/teams/components/Dropdown/dropdownStyles.ts
index f1dea31aab..05b8f30137 100644
--- a/packages/react/src/themes/teams/components/Dropdown/dropdownStyles.ts
+++ b/packages/react/src/themes/teams/components/Dropdown/dropdownStyles.ts
@@ -122,7 +122,6 @@ const dropdownStyles: ComponentSlotStylesInput {
const triggerId = 'triggerElement'
const contentId = 'contentId'
- const testPopupPosition = ({
- align,
- position,
- expectedPlacement,
- rtl = false,
- }: PositionTestInput) =>
- it(`Popup ${position} position is transformed to ${expectedPlacement} Popper's placement`, () => {
- const actualPlacement = getPopupPlacement({ align, position, rtl })
- expect(actualPlacement).toEqual(expectedPlacement)
- })
-
- const testPopupPositionInRtl = ({
- align,
- position,
- expectedPlacement,
- }: PositionTestInput & { rtl?: never }) =>
- testPopupPosition({ align, position, expectedPlacement, rtl: true })
const getPopupContent = (popup: ReactWrapper) => {
return popup.find(`div#${contentId}`)
@@ -76,77 +45,6 @@ describe('Popup', () => {
expect(getPopupContent(popup).exists()).toBe(false)
}
- describe('handles Popup position correctly in ltr', () => {
- testPopupPosition({ position: 'above', align: 'start', expectedPlacement: 'top-start' })
- testPopupPosition({ position: 'above', align: 'center', expectedPlacement: 'top' })
- testPopupPosition({ position: 'above', align: 'end', expectedPlacement: 'top-end' })
- testPopupPosition({ position: 'below', align: 'start', expectedPlacement: 'bottom-start' })
- testPopupPosition({ position: 'below', align: 'center', expectedPlacement: 'bottom' })
- testPopupPosition({ position: 'below', align: 'end', expectedPlacement: 'bottom-end' })
- testPopupPosition({ position: 'before', align: 'top', expectedPlacement: 'left-start' })
- testPopupPosition({ position: 'before', align: 'center', expectedPlacement: 'left' })
- testPopupPosition({ position: 'before', align: 'bottom', expectedPlacement: 'left-end' })
- testPopupPosition({ position: 'after', align: 'top', expectedPlacement: 'right-start' })
- testPopupPosition({ position: 'after', align: 'center', expectedPlacement: 'right' })
- testPopupPosition({ position: 'after', align: 'bottom', expectedPlacement: 'right-end' })
- })
-
- describe('handles Popup position correctly in rtl', () => {
- testPopupPositionInRtl({ position: 'above', align: 'start', expectedPlacement: 'top-end' })
- testPopupPositionInRtl({ position: 'above', align: 'center', expectedPlacement: 'top' })
- testPopupPositionInRtl({ position: 'above', align: 'end', expectedPlacement: 'top-start' })
- testPopupPositionInRtl({ position: 'below', align: 'start', expectedPlacement: 'bottom-end' })
- testPopupPositionInRtl({ position: 'below', align: 'center', expectedPlacement: 'bottom' })
- testPopupPositionInRtl({ position: 'below', align: 'end', expectedPlacement: 'bottom-start' })
- testPopupPositionInRtl({ position: 'before', align: 'top', expectedPlacement: 'right-start' })
- testPopupPositionInRtl({ position: 'before', align: 'center', expectedPlacement: 'right' })
- testPopupPositionInRtl({ position: 'before', align: 'bottom', expectedPlacement: 'right-end' })
- testPopupPositionInRtl({ position: 'after', align: 'top', expectedPlacement: 'left-start' })
- 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')
- })
-
- 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)
- })
- })
-
describe('onOpenChange', () => {
test('is called on click', () => {
const spy = jest.fn()
diff --git a/packages/react/test/specs/lib/positioner/positioningHelper-test.ts b/packages/react/test/specs/lib/positioner/positioningHelper-test.ts
new file mode 100644
index 0000000000..370b6e19dd
--- /dev/null
+++ b/packages/react/test/specs/lib/positioner/positioningHelper-test.ts
@@ -0,0 +1,122 @@
+import { Placement } from 'popper.js'
+
+import { Alignment, Position } from 'src/lib/positioner'
+import { getPlacement, applyRtlToOffset } from 'src/lib/positioner/positioningHelper'
+
+type PositionTestInput = {
+ align: Alignment
+ position: Position
+ expectedPlacement: Placement
+ rtl?: boolean
+}
+
+describe('positioningHelper', () => {
+ const testPositioningHelper = ({
+ align,
+ position,
+ expectedPlacement,
+ rtl = false,
+ }: PositionTestInput) =>
+ it(`positioningHelper ${position} position argument is transformed to ${expectedPlacement} Popper's placement`, () => {
+ const actualPlacement = getPlacement({ align, position, rtl })
+ expect(actualPlacement).toEqual(expectedPlacement)
+ })
+
+ const testPositioningHelperInRtl = ({
+ align,
+ position,
+ expectedPlacement,
+ }: PositionTestInput & { rtl?: never }) =>
+ testPositioningHelper({ align, position, expectedPlacement, rtl: true })
+
+ describe('handles positioningHelper position argument correctly in ltr', () => {
+ testPositioningHelper({ position: 'above', align: 'start', expectedPlacement: 'top-start' })
+ testPositioningHelper({ position: 'above', align: 'center', expectedPlacement: 'top' })
+ testPositioningHelper({ position: 'above', align: 'end', expectedPlacement: 'top-end' })
+ testPositioningHelper({ position: 'below', align: 'start', expectedPlacement: 'bottom-start' })
+ testPositioningHelper({ position: 'below', align: 'center', expectedPlacement: 'bottom' })
+ testPositioningHelper({ position: 'below', align: 'end', expectedPlacement: 'bottom-end' })
+ testPositioningHelper({ position: 'before', align: 'top', expectedPlacement: 'left-start' })
+ testPositioningHelper({ position: 'before', align: 'center', expectedPlacement: 'left' })
+ testPositioningHelper({ position: 'before', align: 'bottom', expectedPlacement: 'left-end' })
+ testPositioningHelper({ position: 'after', align: 'top', expectedPlacement: 'right-start' })
+ testPositioningHelper({ position: 'after', align: 'center', expectedPlacement: 'right' })
+ testPositioningHelper({ position: 'after', align: 'bottom', expectedPlacement: 'right-end' })
+ })
+
+ describe('handles positioningHelper position argument correctly in rtl', () => {
+ testPositioningHelperInRtl({ position: 'above', align: 'start', expectedPlacement: 'top-end' })
+ testPositioningHelperInRtl({ position: 'above', align: 'center', expectedPlacement: 'top' })
+ testPositioningHelperInRtl({ position: 'above', align: 'end', expectedPlacement: 'top-start' })
+ testPositioningHelperInRtl({
+ position: 'below',
+ align: 'start',
+ expectedPlacement: 'bottom-end',
+ })
+ testPositioningHelperInRtl({ position: 'below', align: 'center', expectedPlacement: 'bottom' })
+ testPositioningHelperInRtl({
+ position: 'below',
+ align: 'end',
+ expectedPlacement: 'bottom-start',
+ })
+ testPositioningHelperInRtl({
+ position: 'before',
+ align: 'top',
+ expectedPlacement: 'right-start',
+ })
+ testPositioningHelperInRtl({ position: 'before', align: 'center', expectedPlacement: 'right' })
+ testPositioningHelperInRtl({
+ position: 'before',
+ align: 'bottom',
+ expectedPlacement: 'right-end',
+ })
+ testPositioningHelperInRtl({ position: 'after', align: 'top', expectedPlacement: 'left-start' })
+ testPositioningHelperInRtl({ position: 'after', align: 'center', expectedPlacement: 'left' })
+ testPositioningHelperInRtl({
+ position: 'after',
+ align: 'bottom',
+ expectedPlacement: 'left-end',
+ })
+ })
+
+ describe('positioningHelper offset argument 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')
+ })
+
+ 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)
+ })
+ })
+})