scrollbar.spec.js 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363
  1. import Manipulator from '../../../src/dom/manipulator.js'
  2. import ScrollBarHelper from '../../../src/util/scrollbar.js'
  3. import { clearBodyAndDocument, clearFixture, getFixture } from '../../helpers/fixture.js'
  4. describe('ScrollBar', () => {
  5. let fixtureEl
  6. const doc = document.documentElement
  7. const parseIntDecimal = arg => Number.parseInt(arg, 10)
  8. const getPaddingX = el => parseIntDecimal(window.getComputedStyle(el).paddingRight)
  9. const getMarginX = el => parseIntDecimal(window.getComputedStyle(el).marginRight)
  10. const getOverFlow = el => el.style.overflow
  11. const getPaddingAttr = el => Manipulator.getDataAttribute(el, 'padding-right')
  12. const getMarginAttr = el => Manipulator.getDataAttribute(el, 'margin-right')
  13. const getOverFlowAttr = el => Manipulator.getDataAttribute(el, 'overflow')
  14. const windowCalculations = () => {
  15. return {
  16. htmlClient: document.documentElement.clientWidth,
  17. htmlOffset: document.documentElement.offsetWidth,
  18. docClient: document.body.clientWidth,
  19. htmlBound: document.documentElement.getBoundingClientRect().width,
  20. bodyBound: document.body.getBoundingClientRect().width,
  21. window: window.innerWidth,
  22. width: Math.abs(window.innerWidth - document.documentElement.clientWidth)
  23. }
  24. }
  25. // iOS, Android devices and macOS browsers hide scrollbar by default and show it only while scrolling.
  26. // So the tests for scrollbar would fail
  27. const isScrollBarHidden = () => {
  28. const calc = windowCalculations()
  29. return calc.htmlClient === calc.htmlOffset && calc.htmlClient === calc.window
  30. }
  31. beforeAll(() => {
  32. fixtureEl = getFixture()
  33. // custom fixture to avoid extreme style values
  34. fixtureEl.removeAttribute('style')
  35. })
  36. afterAll(() => {
  37. fixtureEl.remove()
  38. })
  39. afterEach(() => {
  40. clearFixture()
  41. clearBodyAndDocument()
  42. })
  43. beforeEach(() => {
  44. clearBodyAndDocument()
  45. })
  46. describe('isBodyOverflowing', () => {
  47. it('should return true if body is overflowing', () => {
  48. document.documentElement.style.overflowY = 'scroll'
  49. document.body.style.overflowY = 'scroll'
  50. fixtureEl.innerHTML = '<div style="height: 110vh; width: 100%"></div>'
  51. const result = new ScrollBarHelper().isOverflowing()
  52. if (isScrollBarHidden()) {
  53. expect(result).toBeFalse()
  54. } else {
  55. expect(result).toBeTrue()
  56. }
  57. })
  58. it('should return false if body is not overflowing', () => {
  59. doc.style.overflowY = 'hidden'
  60. document.body.style.overflowY = 'hidden'
  61. fixtureEl.innerHTML = '<div style="height: 110vh; width: 100%"></div>'
  62. const scrollBar = new ScrollBarHelper()
  63. const result = scrollBar.isOverflowing()
  64. expect(result).toBeFalse()
  65. })
  66. })
  67. describe('getWidth', () => {
  68. it('should return an integer greater than zero, if body is overflowing', () => {
  69. doc.style.overflowY = 'scroll'
  70. document.body.style.overflowY = 'scroll'
  71. fixtureEl.innerHTML = '<div style="height: 110vh; width: 100%"></div>'
  72. const result = new ScrollBarHelper().getWidth()
  73. if (isScrollBarHidden()) {
  74. expect(result).toEqual(0)
  75. } else {
  76. expect(result).toBeGreaterThan(1)
  77. }
  78. })
  79. it('should return 0 if body is not overflowing', () => {
  80. document.documentElement.style.overflowY = 'hidden'
  81. document.body.style.overflowY = 'hidden'
  82. fixtureEl.innerHTML = '<div style="height: 110vh; width: 100%"></div>'
  83. const result = new ScrollBarHelper().getWidth()
  84. expect(result).toEqual(0)
  85. })
  86. })
  87. describe('hide - reset', () => {
  88. it('should adjust the inline padding of fixed elements which are full-width', () => {
  89. fixtureEl.innerHTML = [
  90. '<div style="height: 110vh; width: 100%">',
  91. ' <div class="fixed-top" id="fixed1" style="padding-right: 0px; width: 100vw"></div>',
  92. ' <div class="fixed-top" id="fixed2" style="padding-right: 5px; width: 100vw"></div>',
  93. '</div>'
  94. ].join('')
  95. doc.style.overflowY = 'scroll'
  96. const fixedEl = fixtureEl.querySelector('#fixed1')
  97. const fixedEl2 = fixtureEl.querySelector('#fixed2')
  98. const originalPadding = getPaddingX(fixedEl)
  99. const originalPadding2 = getPaddingX(fixedEl2)
  100. const scrollBar = new ScrollBarHelper()
  101. const expectedPadding = originalPadding + scrollBar.getWidth()
  102. const expectedPadding2 = originalPadding2 + scrollBar.getWidth()
  103. scrollBar.hide()
  104. let currentPadding = getPaddingX(fixedEl)
  105. let currentPadding2 = getPaddingX(fixedEl2)
  106. expect(getPaddingAttr(fixedEl)).toEqual(`${originalPadding}px`)
  107. expect(getPaddingAttr(fixedEl2)).toEqual(`${originalPadding2}px`)
  108. expect(currentPadding).toEqual(expectedPadding)
  109. expect(currentPadding2).toEqual(expectedPadding2)
  110. scrollBar.reset()
  111. currentPadding = getPaddingX(fixedEl)
  112. currentPadding2 = getPaddingX(fixedEl2)
  113. expect(getPaddingAttr(fixedEl)).toBeNull()
  114. expect(getPaddingAttr(fixedEl2)).toBeNull()
  115. expect(currentPadding).toEqual(originalPadding)
  116. expect(currentPadding2).toEqual(originalPadding2)
  117. })
  118. it('should remove padding & margin if not existed before adjustment', () => {
  119. fixtureEl.innerHTML = [
  120. '<div style="height: 110vh; width: 100%">',
  121. ' <div class="fixed" id="fixed" style="width: 100vw;"></div>',
  122. ' <div class="sticky-top" id="sticky" style=" width: 100vw;"></div>',
  123. '</div>'
  124. ].join('')
  125. doc.style.overflowY = 'scroll'
  126. const fixedEl = fixtureEl.querySelector('#fixed')
  127. const stickyEl = fixtureEl.querySelector('#sticky')
  128. const scrollBar = new ScrollBarHelper()
  129. scrollBar.hide()
  130. scrollBar.reset()
  131. expect(fixedEl.getAttribute('style').includes('padding-right')).toBeFalse()
  132. expect(stickyEl.getAttribute('style').includes('margin-right')).toBeFalse()
  133. })
  134. it('should adjust the inline margin and padding of sticky elements', () => {
  135. fixtureEl.innerHTML = [
  136. '<div style="height: 110vh">',
  137. ' <div class="sticky-top" style="margin-right: 10px; padding-right: 20px; width: 100vw; height: 10px"></div>',
  138. '</div>'
  139. ].join('')
  140. doc.style.overflowY = 'scroll'
  141. const stickyTopEl = fixtureEl.querySelector('.sticky-top')
  142. const originalMargin = getMarginX(stickyTopEl)
  143. const originalPadding = getPaddingX(stickyTopEl)
  144. const scrollBar = new ScrollBarHelper()
  145. const expectedMargin = originalMargin - scrollBar.getWidth()
  146. const expectedPadding = originalPadding + scrollBar.getWidth()
  147. scrollBar.hide()
  148. expect(getMarginAttr(stickyTopEl)).toEqual(`${originalMargin}px`)
  149. expect(getMarginX(stickyTopEl)).toEqual(expectedMargin)
  150. expect(getPaddingAttr(stickyTopEl)).toEqual(`${originalPadding}px`)
  151. expect(getPaddingX(stickyTopEl)).toEqual(expectedPadding)
  152. scrollBar.reset()
  153. expect(getMarginAttr(stickyTopEl)).toBeNull()
  154. expect(getMarginX(stickyTopEl)).toEqual(originalMargin)
  155. expect(getPaddingAttr(stickyTopEl)).toBeNull()
  156. expect(getPaddingX(stickyTopEl)).toEqual(originalPadding)
  157. })
  158. it('should not adjust the inline margin and padding of sticky and fixed elements when element do not have full width', () => {
  159. fixtureEl.innerHTML = '<div class="sticky-top" style="margin-right: 0px; padding-right: 0px; width: 50vw"></div>'
  160. const stickyTopEl = fixtureEl.querySelector('.sticky-top')
  161. const originalMargin = getMarginX(stickyTopEl)
  162. const originalPadding = getPaddingX(stickyTopEl)
  163. const scrollBar = new ScrollBarHelper()
  164. scrollBar.hide()
  165. const currentMargin = getMarginX(stickyTopEl)
  166. const currentPadding = getPaddingX(stickyTopEl)
  167. expect(currentMargin).toEqual(originalMargin)
  168. expect(currentPadding).toEqual(originalPadding)
  169. scrollBar.reset()
  170. })
  171. it('should not put data-attribute if element doesn\'t have the proper style property, should just remove style property if element didn\'t had one', () => {
  172. fixtureEl.innerHTML = [
  173. '<div style="height: 110vh; width: 100%">',
  174. ' <div class="sticky-top" id="sticky" style="width: 100vw"></div>',
  175. '</div>'
  176. ].join('')
  177. document.body.style.overflowY = 'scroll'
  178. const scrollBar = new ScrollBarHelper()
  179. const hasPaddingAttr = el => el.hasAttribute('data-bs-padding-right')
  180. const hasMarginAttr = el => el.hasAttribute('data-bs-margin-right')
  181. const stickyEl = fixtureEl.querySelector('#sticky')
  182. const originalPadding = getPaddingX(stickyEl)
  183. const originalMargin = getMarginX(stickyEl)
  184. const scrollBarWidth = scrollBar.getWidth()
  185. scrollBar.hide()
  186. expect(getPaddingX(stickyEl)).toEqual(scrollBarWidth + originalPadding)
  187. const expectedMargin = scrollBarWidth + originalMargin
  188. expect(getMarginX(stickyEl)).toEqual(expectedMargin === 0 ? expectedMargin : -expectedMargin)
  189. expect(hasMarginAttr(stickyEl)).toBeFalse() // We do not have to keep css margin
  190. expect(hasPaddingAttr(stickyEl)).toBeFalse() // We do not have to keep css padding
  191. scrollBar.reset()
  192. expect(getPaddingX(stickyEl)).toEqual(originalPadding)
  193. expect(getPaddingX(stickyEl)).toEqual(originalPadding)
  194. })
  195. describe('Body Handling', () => {
  196. it('should ignore other inline styles when trying to restore body defaults ', () => {
  197. document.body.style.color = 'red'
  198. const scrollBar = new ScrollBarHelper()
  199. const scrollBarWidth = scrollBar.getWidth()
  200. scrollBar.hide()
  201. expect(getPaddingX(document.body)).toEqual(scrollBarWidth)
  202. expect(document.body.style.color).toEqual('red')
  203. scrollBar.reset()
  204. })
  205. it('should hide scrollbar and reset it to its initial value', () => {
  206. const styleSheetPadding = '7px'
  207. fixtureEl.innerHTML = [
  208. '<style>',
  209. ' body {',
  210. ` padding-right: ${styleSheetPadding}`,
  211. ' }',
  212. '</style>'
  213. ].join('')
  214. const el = document.body
  215. const inlineStylePadding = '10px'
  216. el.style.paddingRight = inlineStylePadding
  217. const originalPadding = getPaddingX(el)
  218. expect(originalPadding).toEqual(parseIntDecimal(inlineStylePadding)) // Respect only the inline style as it has prevails this of css
  219. const originalOverFlow = 'auto'
  220. el.style.overflow = originalOverFlow
  221. const scrollBar = new ScrollBarHelper()
  222. const scrollBarWidth = scrollBar.getWidth()
  223. scrollBar.hide()
  224. const currentPadding = getPaddingX(el)
  225. expect(currentPadding).toEqual(scrollBarWidth + originalPadding)
  226. expect(currentPadding).toEqual(scrollBarWidth + parseIntDecimal(inlineStylePadding))
  227. expect(getPaddingAttr(el)).toEqual(inlineStylePadding)
  228. expect(getOverFlow(el)).toEqual('hidden')
  229. expect(getOverFlowAttr(el)).toEqual(originalOverFlow)
  230. scrollBar.reset()
  231. const currentPadding1 = getPaddingX(el)
  232. expect(currentPadding1).toEqual(originalPadding)
  233. expect(getPaddingAttr(el)).toBeNull()
  234. expect(getOverFlow(el)).toEqual(originalOverFlow)
  235. expect(getOverFlowAttr(el)).toBeNull()
  236. })
  237. it('should hide scrollbar and reset it to its initial value - respecting css rules', () => {
  238. const styleSheetPadding = '7px'
  239. fixtureEl.innerHTML = [
  240. '<style>',
  241. ' body {',
  242. ` padding-right: ${styleSheetPadding}`,
  243. ' }',
  244. '</style>'
  245. ].join('')
  246. const el = document.body
  247. const originalPadding = getPaddingX(el)
  248. const originalOverFlow = 'scroll'
  249. el.style.overflow = originalOverFlow
  250. const scrollBar = new ScrollBarHelper()
  251. const scrollBarWidth = scrollBar.getWidth()
  252. scrollBar.hide()
  253. const currentPadding = getPaddingX(el)
  254. expect(currentPadding).toEqual(scrollBarWidth + originalPadding)
  255. expect(currentPadding).toEqual(scrollBarWidth + parseIntDecimal(styleSheetPadding))
  256. expect(getPaddingAttr(el)).toBeNull() // We do not have to keep css padding
  257. expect(getOverFlow(el)).toEqual('hidden')
  258. expect(getOverFlowAttr(el)).toEqual(originalOverFlow)
  259. scrollBar.reset()
  260. const currentPadding1 = getPaddingX(el)
  261. expect(currentPadding1).toEqual(originalPadding)
  262. expect(getPaddingAttr(el)).toBeNull()
  263. expect(getOverFlow(el)).toEqual(originalOverFlow)
  264. expect(getOverFlowAttr(el)).toBeNull()
  265. })
  266. it('should not adjust the inline body padding when it does not overflow', () => {
  267. const originalPadding = getPaddingX(document.body)
  268. const scrollBar = new ScrollBarHelper()
  269. // Hide scrollbars to prevent the body overflowing
  270. doc.style.overflowY = 'hidden'
  271. doc.style.paddingRight = '0px'
  272. scrollBar.hide()
  273. const currentPadding = getPaddingX(document.body)
  274. expect(currentPadding).toEqual(originalPadding)
  275. scrollBar.reset()
  276. })
  277. it('should not adjust the inline body padding when it does not overflow, even on a scaled display', () => {
  278. const originalPadding = getPaddingX(document.body)
  279. const scrollBar = new ScrollBarHelper()
  280. // Remove body margins as would be done by Bootstrap css
  281. document.body.style.margin = '0'
  282. // Hide scrollbars to prevent the body overflowing
  283. doc.style.overflowY = 'hidden'
  284. // Simulate a discrepancy between exact, i.e. floating point body width, and rounded body width
  285. // as it can occur when zooming or scaling the display to something else than 100%
  286. doc.style.paddingRight = '.48px'
  287. scrollBar.hide()
  288. const currentPadding = getPaddingX(document.body)
  289. expect(currentPadding).toEqual(originalPadding)
  290. scrollBar.reset()
  291. })
  292. })
  293. })
  294. })