position.js 9.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461
  1. /**
  2. * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
  3. * For licensing, see LICENSE.md.
  4. */
  5. import global from '../../src/dom/global';
  6. import { getOptimalPosition } from '../../src/dom/position';
  7. let element, target, limiter;
  8. describe( 'getOptimalPosition()', () => {
  9. beforeEach( () => {
  10. stubWindow( {
  11. innerWidth: 10000,
  12. innerHeight: 10000,
  13. scrollX: 0,
  14. scrollY: 0
  15. } );
  16. } );
  17. describe( 'for single position', () => {
  18. beforeEach( setElementTargetPlayground );
  19. it( 'should return coordinates', () => {
  20. assertPosition( { element, target, positions: [ attachLeft ] }, {
  21. top: 100,
  22. left: 80,
  23. name: 'left'
  24. } );
  25. } );
  26. it( 'should return coordinates (window scroll)', () => {
  27. stubWindow( {
  28. innerWidth: 10000,
  29. innerHeight: 10000,
  30. scrollX: 100,
  31. scrollY: 100,
  32. } );
  33. assertPosition( { element, target, positions: [ attachLeft ] }, {
  34. top: 200,
  35. left: 180,
  36. name: 'left'
  37. } );
  38. } );
  39. describe( 'positioned element parent', () => {
  40. let parent;
  41. it( 'should return coordinates', () => {
  42. stubWindow( {
  43. innerWidth: 10000,
  44. innerHeight: 10000,
  45. scrollX: 1000,
  46. scrollY: 1000
  47. } );
  48. parent = getElement( {
  49. top: 1000,
  50. right: 1010,
  51. bottom: 1010,
  52. left: 1000,
  53. width: 10,
  54. height: 10
  55. }, {
  56. position: 'absolute'
  57. } );
  58. element.parentElement = parent;
  59. assertPosition( { element, target, positions: [ attachLeft ] }, {
  60. top: -900,
  61. left: -920,
  62. name: 'left'
  63. } );
  64. } );
  65. it( 'should return coordinates (scroll and border)', () => {
  66. stubWindow( {
  67. innerWidth: 10000,
  68. innerHeight: 10000,
  69. scrollX: 1000,
  70. scrollY: 1000
  71. } );
  72. parent = getElement( {
  73. top: 0,
  74. right: 10,
  75. bottom: 10,
  76. left: 0,
  77. width: 10,
  78. height: 10,
  79. scrollTop: 100,
  80. scrollLeft: 200
  81. }, {
  82. position: 'absolute',
  83. borderLeftWidth: '20px',
  84. borderTopWidth: '40px',
  85. } );
  86. element.parentElement = parent;
  87. assertPosition( { element, target, positions: [ attachLeft ] }, {
  88. top: 160,
  89. left: 260,
  90. name: 'left'
  91. } );
  92. } );
  93. } );
  94. } );
  95. describe( 'for multiple positions', () => {
  96. beforeEach( setElementTargetPlayground );
  97. it( 'should return coordinates', () => {
  98. assertPosition( {
  99. element, target,
  100. positions: [ attachLeft, attachRight ]
  101. }, {
  102. top: 100,
  103. left: 80,
  104. name: 'left'
  105. } );
  106. } );
  107. it( 'should return coordinates (position preference order)', () => {
  108. assertPosition( {
  109. element, target,
  110. positions: [ attachRight, attachLeft ]
  111. }, {
  112. top: 100,
  113. left: 110,
  114. name: 'right'
  115. } );
  116. } );
  117. } );
  118. describe( 'with a limiter', () => {
  119. beforeEach( setElementTargetLimiterPlayground );
  120. it( 'should return coordinates (#1)', () => {
  121. assertPosition( {
  122. element, target, limiter,
  123. positions: [ attachLeft, attachRight ]
  124. }, {
  125. top: 100,
  126. left: -20,
  127. name: 'left'
  128. } );
  129. } );
  130. it( 'should return coordinates (#2)', () => {
  131. assertPosition( {
  132. element, target, limiter,
  133. positions: [ attachRight, attachLeft ]
  134. }, {
  135. top: 100,
  136. left: -20,
  137. name: 'left'
  138. } );
  139. } );
  140. } );
  141. describe( 'with fitInViewport on', () => {
  142. beforeEach( setElementTargetLimiterPlayground );
  143. it( 'should return coordinates (#1)', () => {
  144. assertPosition( {
  145. element, target,
  146. positions: [ attachLeft, attachRight ],
  147. fitInViewport: true
  148. }, {
  149. top: 100,
  150. left: 10,
  151. name: 'right'
  152. } );
  153. } );
  154. it( 'should return coordinates (#2)', () => {
  155. assertPosition( {
  156. element, target,
  157. positions: [ attachRight, attachLeft ],
  158. fitInViewport: true
  159. }, {
  160. top: 100,
  161. left: 10,
  162. name: 'right'
  163. } );
  164. } );
  165. it( 'should return coordinates (#3)', () => {
  166. assertPosition( {
  167. element, target,
  168. positions: [ attachLeft, attachBottom, attachRight ],
  169. fitInViewport: true
  170. }, {
  171. top: 110,
  172. left: 0,
  173. name: 'bottom'
  174. } );
  175. } );
  176. } );
  177. describe( 'with limiter and fitInViewport on', () => {
  178. beforeEach( setElementTargetLimiterPlayground );
  179. it( 'should return coordinates (#1)', () => {
  180. assertPosition( {
  181. element, target, limiter,
  182. positions: [ attachLeft, attachRight ],
  183. fitInViewport: true
  184. }, {
  185. top: 100,
  186. left: 10,
  187. name: 'right'
  188. } );
  189. } );
  190. it( 'should return coordinates (#2)', () => {
  191. assertPosition( {
  192. element, target, limiter,
  193. positions: [ attachRight, attachLeft ],
  194. fitInViewport: true
  195. }, {
  196. top: 100,
  197. left: 10,
  198. name: 'right'
  199. } );
  200. } );
  201. it( 'should return coordinates (#3)', () => {
  202. assertPosition( {
  203. element, target, limiter,
  204. positions: [ attachRight, attachLeft, attachBottom ],
  205. fitInViewport: true
  206. }, {
  207. top: 110,
  208. left: 0,
  209. name: 'bottom'
  210. } );
  211. } );
  212. it( 'should return coordinates (#4)', () => {
  213. assertPosition( {
  214. element, target, limiter,
  215. positions: [ attachTop, attachRight ],
  216. fitInViewport: true
  217. }, {
  218. top: 100,
  219. left: 10,
  220. name: 'right'
  221. } );
  222. } );
  223. it( 'should return the very first coordinates if no fitting position with a positive intersection has been found', () => {
  224. assertPosition( {
  225. element, target, limiter,
  226. positions: [
  227. () => ( {
  228. left: -10000,
  229. top: -10000,
  230. name: 'no-intersect-position'
  231. } )
  232. ],
  233. fitInViewport: true
  234. }, {
  235. left: -10000,
  236. top: -10000,
  237. name: 'no-intersect-position'
  238. } );
  239. } );
  240. it( 'should return the very first coordinates if limiter does not fit into the viewport', () => {
  241. getElement( limiter, {
  242. top: -100,
  243. right: -80,
  244. bottom: -80,
  245. left: -100,
  246. width: 20,
  247. height: 20
  248. } );
  249. assertPosition( {
  250. element, target, limiter,
  251. positions: [ attachRight, attachTop ],
  252. fitInViewport: true
  253. }, {
  254. top: 100,
  255. left: 10,
  256. name: 'right'
  257. } );
  258. } );
  259. } );
  260. } );
  261. function assertPosition( options, expected ) {
  262. const position = getOptimalPosition( options );
  263. expect( position ).to.deep.equal( expected );
  264. }
  265. // +--------+-----+
  266. // | E | T |
  267. // +--------+-----+
  268. const attachLeft = ( targetRect, elementRect ) => ( {
  269. top: targetRect.top,
  270. left: targetRect.left - elementRect.width,
  271. name: 'left'
  272. } );
  273. // +-----+--------+
  274. // | T | E |
  275. // +-----+--------+
  276. const attachRight = ( targetRect ) => ( {
  277. top: targetRect.top,
  278. left: targetRect.left + targetRect.width,
  279. name: 'right'
  280. } );
  281. // +-----+
  282. // | T |
  283. // +-----+--+
  284. // | E |
  285. // +--------+
  286. const attachBottom = ( targetRect ) => ( {
  287. top: targetRect.bottom,
  288. left: targetRect.left,
  289. name: 'bottom'
  290. } );
  291. // +--------+
  292. // | E |
  293. // +--+-----+
  294. // | T |
  295. // +-----+
  296. const attachTop = ( targetRect, elementRect ) => ( {
  297. top: targetRect.top - elementRect.height,
  298. left: targetRect.left - ( elementRect.width - targetRect.width ),
  299. name: 'bottom'
  300. } );
  301. // Returns a synthetic element.
  302. //
  303. // @private
  304. // @param {Object} properties A set of properties for the element.
  305. // @param {Object} styles A set of styles in `window.getComputedStyle()` format.
  306. function getElement( properties = {}, styles = {} ) {
  307. const element = {
  308. tagName: 'div',
  309. scrollLeft: 0,
  310. scrollTop: 0
  311. };
  312. Object.assign( element, properties );
  313. if ( !styles.borderLeftWidth ) {
  314. styles.borderLeftWidth = '0px';
  315. }
  316. if ( !styles.borderTopWidth ) {
  317. styles.borderTopWidth = '0px';
  318. }
  319. global.window.getComputedStyle.withArgs( element ).returns( styles );
  320. return element;
  321. }
  322. // Stubs the window.
  323. //
  324. // @private
  325. // @param {Object} properties A set of properties the window should have.
  326. function stubWindow( properties ) {
  327. global.window = Object.assign( {
  328. getComputedStyle: sinon.stub()
  329. }, properties );
  330. }
  331. // <-- 100px ->
  332. //
  333. // ^ +--------------[ Viewport ]----------
  334. // | |
  335. // 100px |
  336. // | | <-- 10px -->
  337. // V |
  338. // | ^ +---------+
  339. // | | | |
  340. // | 10px | T |
  341. // | | | |
  342. // | V +---------+
  343. // |
  344. //
  345. function setElementTargetPlayground() {
  346. element = getElement( {
  347. top: 0,
  348. right: 20,
  349. bottom: 20,
  350. left: 0,
  351. width: 20,
  352. height: 20
  353. } );
  354. target = getElement( {
  355. top: 100,
  356. right: 110,
  357. bottom: 110,
  358. left: 100,
  359. width: 10,
  360. height: 10
  361. } );
  362. }
  363. //
  364. //
  365. // ^ +-----------[ Viewport ]----------------------
  366. // | |
  367. // 100px |
  368. // | <--------- 20px ------->
  369. // | <-- 10px -->
  370. // V |
  371. // +------------+---------+ ^ ^
  372. // | | | | |
  373. // | | T | 10px |
  374. // | | | | |
  375. // | +---------+ V 20px
  376. // | | | |
  377. // | | | |
  378. // | | | |
  379. // +------[ Limiter ]-----+ V
  380. // |
  381. // |
  382. //
  383. //
  384. function setElementTargetLimiterPlayground() {
  385. element = getElement( {
  386. top: 0,
  387. right: 20,
  388. bottom: 20,
  389. left: 0,
  390. width: 20,
  391. height: 20
  392. } );
  393. limiter = getElement( {
  394. top: 100,
  395. right: 10,
  396. bottom: 120,
  397. left: -10,
  398. width: 20,
  399. height: 20
  400. } );
  401. target = getElement( {
  402. top: 100,
  403. right: 10,
  404. bottom: 110,
  405. left: 0,
  406. width: 10,
  407. height: 10
  408. } );
  409. }