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) + }) + }) +})