11import type { VueWrapper } from '@vue/test-utils'
22import { mount } from '@vue/test-utils'
33import { beforeEach , describe , expect , it } from 'vitest'
4+ import { defineComponent , h } from 'vue'
45import Popper from './_Popper.vue'
6+ import PopperAnchor from './PopperAnchor.vue'
7+ import PopperContent from './PopperContent.vue'
8+ import PopperRoot from './PopperRoot.vue'
9+ import { transformOrigin } from './utils'
510
611describe ( 'give default Popper' , async ( ) => {
712 let wrapper : VueWrapper < InstanceType < typeof Popper > >
@@ -19,3 +24,152 @@ describe('give default Popper', async () => {
1924 expect ( wrapper . element ) . toMatchSnapshot ( )
2025 } )
2126} )
27+
28+ describe ( 'transformOrigin direction-aware logic' , ( ) => {
29+ async function run (
30+ options : Parameters < typeof transformOrigin > [ 0 ] ,
31+ placement : string ,
32+ arrowData ?: { x ?: number , y ?: number , centerOffset ?: number } ,
33+ ) {
34+ const result = await transformOrigin ( options ) . fn ( {
35+ placement,
36+ rects : { floating : { width : 100 , height : 50 } } ,
37+ middlewareData : { arrow : arrowData } ,
38+ } as any )
39+ return result . data as { x : string , y : string }
40+ }
41+
42+ const noArrow = { arrowWidth : 0 , arrowHeight : 0 }
43+
44+ describe ( 'bottom placement' , ( ) => {
45+ it ( 'rTL flips start origin to 100%' , async ( ) => {
46+ expect ( ( await run ( { ...noArrow , dir : 'rtl' } , 'bottom-start' ) ) . x ) . toBe ( '100%' )
47+ } )
48+ it ( 'lTR keeps start origin at 0%' , async ( ) => {
49+ expect ( ( await run ( { ...noArrow , dir : 'ltr' } , 'bottom-start' ) ) . x ) . toBe ( '0%' )
50+ } )
51+ it ( 'rTL flips end origin to 0%' , async ( ) => {
52+ expect ( ( await run ( { ...noArrow , dir : 'rtl' } , 'bottom-end' ) ) . x ) . toBe ( '0%' )
53+ } )
54+ it ( 'lTR keeps end origin at 100%' , async ( ) => {
55+ expect ( ( await run ( { ...noArrow , dir : 'ltr' } , 'bottom-end' ) ) . x ) . toBe ( '100%' )
56+ } )
57+ it ( 'center is unaffected by dir' , async ( ) => {
58+ expect ( ( await run ( { ...noArrow , dir : 'rtl' } , 'bottom' ) ) . x ) . toBe ( '50%' )
59+ expect ( ( await run ( { ...noArrow , dir : 'ltr' } , 'bottom' ) ) . x ) . toBe ( '50%' )
60+ } )
61+ } )
62+
63+ describe ( 'top placement' , ( ) => {
64+ it ( 'rTL flips start origin to 100%' , async ( ) => {
65+ expect ( ( await run ( { ...noArrow , dir : 'rtl' } , 'top-start' ) ) . x ) . toBe ( '100%' )
66+ } )
67+ it ( 'lTR keeps start origin at 0%' , async ( ) => {
68+ expect ( ( await run ( { ...noArrow , dir : 'ltr' } , 'top-start' ) ) . x ) . toBe ( '0%' )
69+ } )
70+ it ( 'rTL flips end origin to 0%' , async ( ) => {
71+ expect ( ( await run ( { ...noArrow , dir : 'rtl' } , 'top-end' ) ) . x ) . toBe ( '0%' )
72+ } )
73+ it ( 'lTR keeps end origin at 100%' , async ( ) => {
74+ expect ( ( await run ( { ...noArrow , dir : 'ltr' } , 'top-end' ) ) . x ) . toBe ( '100%' )
75+ } )
76+ } )
77+
78+ describe ( 'left/right placements use Y-axis alignment, unaffected by dir' , ( ) => {
79+ it ( 'right-start: y origin is 0% for both RTL and LTR' , async ( ) => {
80+ expect ( ( await run ( { ...noArrow , dir : 'rtl' } , 'right-start' ) ) . y ) . toBe ( '0%' )
81+ expect ( ( await run ( { ...noArrow , dir : 'ltr' } , 'right-start' ) ) . y ) . toBe ( '0%' )
82+ } )
83+ it ( 'right-end: y origin is 100% for both RTL and LTR' , async ( ) => {
84+ expect ( ( await run ( { ...noArrow , dir : 'rtl' } , 'right-end' ) ) . y ) . toBe ( '100%' )
85+ expect ( ( await run ( { ...noArrow , dir : 'ltr' } , 'right-end' ) ) . y ) . toBe ( '100%' )
86+ } )
87+ it ( 'left-start: y origin is 0% for both RTL and LTR' , async ( ) => {
88+ expect ( ( await run ( { ...noArrow , dir : 'rtl' } , 'left-start' ) ) . y ) . toBe ( '0%' )
89+ expect ( ( await run ( { ...noArrow , dir : 'ltr' } , 'left-start' ) ) . y ) . toBe ( '0%' )
90+ } )
91+ it ( 'left-end: y origin is 100% for both RTL and LTR' , async ( ) => {
92+ expect ( ( await run ( { ...noArrow , dir : 'rtl' } , 'left-end' ) ) . y ) . toBe ( '100%' )
93+ expect ( ( await run ( { ...noArrow , dir : 'ltr' } , 'left-end' ) ) . y ) . toBe ( '100%' )
94+ } )
95+ } )
96+
97+ describe ( 'default dir (undefined)' , ( ) => {
98+ it ( 'bottom placement defaults to LTR: start → 0%, end → 100%' , async ( ) => {
99+ expect ( ( await run ( noArrow , 'bottom-start' ) ) . x ) . toBe ( '0%' )
100+ expect ( ( await run ( noArrow , 'bottom-end' ) ) . x ) . toBe ( '100%' )
101+ } )
102+ it ( 'top placement defaults to LTR: start → 0%, end → 100%' , async ( ) => {
103+ expect ( ( await run ( noArrow , 'top-start' ) ) . x ) . toBe ( '0%' )
104+ expect ( ( await run ( noArrow , 'top-end' ) ) . x ) . toBe ( '100%' )
105+ } )
106+ } )
107+
108+ describe ( 'arrow-visible scenario: arrow-based x/y positioning is not affected by dir' , ( ) => {
109+ // arrowXCenter = arrowData.x (20) + arrowWidth (10) / 2 = 25
110+ // arrowYCenter = arrowData.y (15) + arrowHeight (5) / 2 = 17.5
111+ const withArrow = { arrowWidth : 10 , arrowHeight : 5 }
112+ const arrowData = { x : 20 , y : 15 , centerOffset : 0 }
113+
114+ it ( 'bottom placement uses arrow x center regardless of dir' , async ( ) => {
115+ expect ( ( await run ( { ...withArrow , dir : 'rtl' } , 'bottom-start' , arrowData ) ) . x ) . toBe ( '25px' )
116+ expect ( ( await run ( { ...withArrow , dir : 'ltr' } , 'bottom-start' , arrowData ) ) . x ) . toBe ( '25px' )
117+ } )
118+ it ( 'top placement uses arrow x center regardless of dir' , async ( ) => {
119+ expect ( ( await run ( { ...withArrow , dir : 'rtl' } , 'top-end' , arrowData ) ) . x ) . toBe ( '25px' )
120+ expect ( ( await run ( { ...withArrow , dir : 'ltr' } , 'top-end' , arrowData ) ) . x ) . toBe ( '25px' )
121+ } )
122+ it ( 'right placement uses arrow y center regardless of dir' , async ( ) => {
123+ expect ( ( await run ( { ...withArrow , dir : 'rtl' } , 'right-start' , arrowData ) ) . y ) . toBe ( '17.5px' )
124+ expect ( ( await run ( { ...withArrow , dir : 'ltr' } , 'right-start' , arrowData ) ) . y ) . toBe ( '17.5px' )
125+ } )
126+ it ( 'left placement uses arrow y center regardless of dir' , async ( ) => {
127+ expect ( ( await run ( { ...withArrow , dir : 'rtl' } , 'left-end' , arrowData ) ) . y ) . toBe ( '17.5px' )
128+ expect ( ( await run ( { ...withArrow , dir : 'ltr' } , 'left-end' , arrowData ) ) . y ) . toBe ( '17.5px' )
129+ } )
130+ } )
131+ } )
132+
133+ describe ( 'popper wrapper dir attribute' , ( ) => {
134+ globalThis . ResizeObserver = class ResizeObserver {
135+ observe ( ) { }
136+ unobserve ( ) { }
137+ disconnect ( ) { }
138+ }
139+
140+ it ( 'sets dir on the floatingRef wrapper so placement and origin are consistent' , async ( ) => {
141+ const RtlPopper = defineComponent ( {
142+ setup ( ) {
143+ return ( ) =>
144+ h ( PopperRoot , null , {
145+ default : ( ) => [
146+ h ( PopperAnchor , null , { default : ( ) => 'Anchor' } ) ,
147+ h ( PopperContent , { dir : 'rtl' } , { default : ( ) => 'Content' } ) ,
148+ ] ,
149+ } )
150+ } ,
151+ } )
152+
153+ const wrapper = mount ( RtlPopper , { attachTo : document . body } )
154+ const wrapperDiv = wrapper . find ( '[data-reka-popper-content-wrapper]' )
155+ expect ( wrapperDiv . attributes ( 'dir' ) ) . toBe ( 'rtl' )
156+ } )
157+
158+ it ( 'sets dir=ltr on the wrapper when explicitly passed' , async ( ) => {
159+ const LtrPopper = defineComponent ( {
160+ setup ( ) {
161+ return ( ) =>
162+ h ( PopperRoot , null , {
163+ default : ( ) => [
164+ h ( PopperAnchor , null , { default : ( ) => 'Anchor' } ) ,
165+ h ( PopperContent , { dir : 'ltr' } , { default : ( ) => 'Content' } ) ,
166+ ] ,
167+ } )
168+ } ,
169+ } )
170+
171+ const wrapper = mount ( LtrPopper , { attachTo : document . body } )
172+ const wrapperDiv = wrapper . find ( '[data-reka-popper-content-wrapper]' )
173+ expect ( wrapperDiv . attributes ( 'dir' ) ) . toBe ( 'ltr' )
174+ } )
175+ } )
0 commit comments