aboutsummaryrefslogtreecommitdiff
path: root/js
diff options
context:
space:
mode:
Diffstat (limited to 'js')
-rw-r--r--js/src/scrollspy.js56
-rw-r--r--js/tests/unit/scrollspy.spec.js87
2 files changed, 77 insertions, 66 deletions
diff --git a/js/src/scrollspy.js b/js/src/scrollspy.js
index 9809c28cd..173557622 100644
--- a/js/src/scrollspy.js
+++ b/js/src/scrollspy.js
@@ -43,7 +43,7 @@ const CLASS_NAME_ACTIVE = 'active'
const SELECTOR_DATA_SPY = '[data-bs-spy="scroll"]'
const SELECTOR_NAV_LIST_GROUP = '.nav, .list-group'
const SELECTOR_NAV_LINKS = '.nav-link'
-// const SELECTOR_NAV_ITEMS = '.nav-item'
+const SELECTOR_NAV_ITEMS = '.nav-item'
const SELECTOR_LIST_ITEMS = '.list-group-item'
const SELECTOR_DROPDOWN = '.dropdown'
const SELECTOR_DROPDOWN_TOGGLE = '.dropdown-toggle'
@@ -95,7 +95,9 @@ class ScrollSpy extends BaseComponent {
this._observer = this._getNewObserver()
}
- this._observableSections.forEach(section => this._observer.observe(section))
+ for (const section of this._observableSections) {
+ this._observer.observe(section)
+ }
}
dispose() {
@@ -136,13 +138,21 @@ class ScrollSpy extends BaseComponent {
if (target.classList.contains(CLASS_NAME_DROPDOWN_ITEM)) { // Activate dropdown parents
SelectorEngine.findOne(SELECTOR_DROPDOWN_TOGGLE, target.closest(SELECTOR_DROPDOWN))
.classList.add(CLASS_NAME_ACTIVE)
- }
+ } else {
+ for (const listGroup of SelectorEngine.parents(target, SELECTOR_NAV_LIST_GROUP)) {
+ // Set triggered links parents as active
+ // With both <ul> and <nav> markup a parent is the previous sibling of any nav ancestor
+ for (const item of SelectorEngine.prev(listGroup, `${SELECTOR_NAV_LINKS}, ${SELECTOR_LIST_ITEMS}`)) {
+ item.classList.add(CLASS_NAME_ACTIVE)
+ }
- for (const listGroup of SelectorEngine.parents(target, SELECTOR_NAV_LIST_GROUP)) {
- // Set triggered links parents as active
- // With both <ul> and <nav> markup a parent is the previous sibling of any nav ancestor
- SelectorEngine.prev(listGroup, `${SELECTOR_NAV_LINKS}, ${SELECTOR_LIST_ITEMS}`)
- .forEach(item => item.classList.add(CLASS_NAME_ACTIVE))
+ // Handle special case when .nav-link is inside .nav-item
+ for (const navItem of SelectorEngine.prev(listGroup, SELECTOR_NAV_ITEMS)) {
+ for (const item of SelectorEngine.children(navItem, SELECTOR_NAV_LINKS)) {
+ item.classList.add(CLASS_NAME_ACTIVE)
+ }
+ }
+ }
}
EventHandler.trigger(this._element, EVENT_ACTIVATE, {
@@ -155,42 +165,47 @@ class ScrollSpy extends BaseComponent {
parent.classList.remove(CLASS_NAME_ACTIVE)
}
- SelectorEngine.find(`.${CLASS_NAME_ACTIVE}`, parent)
- .forEach(node => node.classList.remove(CLASS_NAME_ACTIVE))
+ for (const node of SelectorEngine.find(`.${CLASS_NAME_ACTIVE}`, parent)) {
+ node.classList.remove(CLASS_NAME_ACTIVE)
+ }
}
_getNewObserver() {
let previousVisibleEntryTop = 0
let previousParentScrollTop = 0
+ const getTargetLink = entry => this._targetLinks.find(el => el.hash === `#${entry.target.id}`)
+
const activate = entry => {
previousVisibleEntryTop = entry.target.offsetTop
- const targetToActivate = this._targetLinks.find(el => el.hash === `#${entry.target.id}`)
+ const targetToActivate = getTargetLink(entry)
this._process(targetToActivate)
}
const callback = entries => {
const parentScrollTop = this._element.scrollTop
- entries.forEach(entry => {
- if (entry.isIntersecting) {
+ let previousIntersectionRatio = 0
+ for (const entry of entries) {
+ if (entry.isIntersecting && previousIntersectionRatio < entry.intersectionRatio) {
const { offsetTop } = entry.target
+ previousIntersectionRatio = entry.intersectionRatio
const userScrollsDown = parentScrollTop >= previousParentScrollTop
if (userScrollsDown && offsetTop >= previousVisibleEntryTop) { // if we are scrolling down, pick the bigger offsetTop
activate(entry)
- return
+ continue
}
- if (!userScrollsDown && offsetTop < previousVisibleEntryTop) {// if we are scrolling up, pick the smallest offsetTop
+ if (!userScrollsDown && offsetTop < previousVisibleEntryTop) { // if we are scrolling up, pick the smallest offsetTop
activate(entry)
}
- return
+ continue
}
- const notVisibleElement = this._targetLinks.find(el => el.hash === `#${entry.target.id}`)
+ const notVisibleElement = getTargetLink(entry)
this._clearActiveClass(notVisibleElement)
- })
+ }
previousParentScrollTop = this._element.scrollTop
}
@@ -236,8 +251,9 @@ class ScrollSpy extends BaseComponent {
*/
EventHandler.on(window, EVENT_LOAD_DATA_API, () => {
- SelectorEngine.find(SELECTOR_DATA_SPY)
- .forEach(spy => new ScrollSpy(spy))
+ for (const spy of SelectorEngine.find(SELECTOR_DATA_SPY)) {
+ new ScrollSpy(spy) // eslint-disable-line no-new
+ }
})
/**
diff --git a/js/tests/unit/scrollspy.spec.js b/js/tests/unit/scrollspy.spec.js
index 248639e17..3412efaa1 100644
--- a/js/tests/unit/scrollspy.spec.js
+++ b/js/tests/unit/scrollspy.spec.js
@@ -7,6 +7,11 @@ import { clearFixture, createEvent, getFixture, jQueryMock } from '../helpers/fi
describe('ScrollSpy', () => {
let fixtureEl
+ const scrollTo = (el, height) => {
+ // el.scrollTo({ top: height })
+ el.scrollTop = height
+ }
+
const onScrollStop = (callback, element, timeout = 30) => {
let handle = null
const onScroll = function () {
@@ -36,25 +41,29 @@ describe('ScrollSpy', () => {
].join('')
}
- const testElementIsActiveAfterScroll = ({ elementSelector, targetSelector, contentEl, scrollSpy, spy, cb }) => {
+ const testElementIsActiveAfterScroll = ({ elementSelector, targetSelector, contentEl, scrollSpy, cb }) => {
const element = fixtureEl.querySelector(elementSelector)
const target = fixtureEl.querySelector(targetSelector)
// add top padding to fix Chrome on Android failures
- const paddingTop = 5
+ const paddingTop = 0
const parentOffset = getComputedStyle(contentEl).getPropertyValue('position') === 'relative' ? 0 : contentEl.offsetTop
const scrollHeight = (target.offsetTop - parentOffset) + paddingTop
- function listener() {
+ contentEl.addEventListener('activate.bs.scrollspy', event => {
+
+ if (scrollSpy._activeTarget !== element) {
+ return
+ }
+
expect(element.classList.contains('active')).toEqual(true)
expect(scrollSpy._activeTarget).toEqual(element)
- spy.calls.reset()
+ expect(event.relatedTarget).toEqual(element)
cb()
- }
+ })
setTimeout(() => { // in case we scroll something before the test
- onScrollStop(listener, contentEl)
- contentEl.scrollTo({ top: scrollHeight })
- }, 50)
+ scrollTo(contentEl, scrollHeight)
+ }, 100)
}
beforeAll(() => {
@@ -151,7 +160,7 @@ describe('ScrollSpy', () => {
done()
}, scrollSpyEl)
- scrollSpyEl.scrollTo({ top: 350 })
+ scrollTo(scrollSpyEl, 350)
})
it('should only switch "active" class on current target specified w element', done => {
@@ -189,7 +198,7 @@ describe('ScrollSpy', () => {
done()
}, scrollSpyEl)
- scrollSpyEl.scrollTo({ top: 350 })
+ scrollTo(scrollSpyEl, 350)
})
it('should correctly select middle navigation option when large offset is used', done => {
@@ -225,7 +234,7 @@ describe('ScrollSpy', () => {
done()
}, contentEl)
- contentEl.scrollTo({ top: 550 })
+ scrollTo(contentEl, 550)
})
it('should add the active class to the correct element', done => {
@@ -247,22 +256,19 @@ describe('ScrollSpy', () => {
offset: 0,
target: '.navbar'
})
- const spy = spyOn(scrollSpy, '_process').and.callThrough()
testElementIsActiveAfterScroll({
elementSelector: '#a-1',
targetSelector: '#div-1',
contentEl,
scrollSpy,
- spy,
cb: () => {
testElementIsActiveAfterScroll({
elementSelector: '#a-2',
targetSelector: '#div-2',
contentEl,
scrollSpy,
- spy,
- cb: () => done()
+ cb: done
})
}
})
@@ -287,22 +293,19 @@ describe('ScrollSpy', () => {
offset: 0,
target: '.navbar'
})
- const spy = spyOn(scrollSpy, '_process').and.callThrough()
testElementIsActiveAfterScroll({
elementSelector: '#a-1',
targetSelector: '#div-1',
contentEl,
scrollSpy,
- spy,
cb: () => {
testElementIsActiveAfterScroll({
elementSelector: '#a-2',
targetSelector: '#div-2',
contentEl,
scrollSpy,
- spy,
- cb: () => done()
+ cb: done
})
}
})
@@ -327,22 +330,19 @@ describe('ScrollSpy', () => {
offset: 0,
target: '.navbar'
})
- const spy = spyOn(scrollSpy, '_process').and.callThrough()
testElementIsActiveAfterScroll({
elementSelector: '#a-1',
targetSelector: '#div-1',
contentEl,
scrollSpy,
- spy,
cb: () => {
testElementIsActiveAfterScroll({
elementSelector: '#a-2',
targetSelector: '#div-2',
contentEl,
scrollSpy,
- spy,
- cb: () => done()
+ cb: done
})
}
})
@@ -382,10 +382,10 @@ describe('ScrollSpy', () => {
expect(active()).toBeNull()
done()
}, contentEl)
- contentEl.scrollTo({ top: 0 })
+ scrollTo(contentEl, 0)
}, contentEl)
- contentEl.scrollTo({ top: 201 })
+ scrollTo(contentEl, 201)
})
it('should not clear selection if above the first section and first section is at the top', done => {
@@ -428,10 +428,10 @@ describe('ScrollSpy', () => {
done()
}, contentEl)
- contentEl.scrollTo({ top: 0 })
+ scrollTo(contentEl, 0)
}, contentEl)
- contentEl.scrollTo({ top: startOfSectionTwo })
+ scrollTo(contentEl, startOfSectionTwo)
})
it('should correctly select navigation element on backward scrolling when each target section height is 100%', done => {
@@ -459,46 +459,41 @@ describe('ScrollSpy', () => {
offset: 0,
target: '.navbar'
})
- const spy = spyOn(scrollSpy, '_process').and.callThrough()
+ scrollTo(contentEl, 0)
testElementIsActiveAfterScroll({
elementSelector: '#li-100-5',
targetSelector: '#div-100-5',
- scrollSpy,
- spy,
contentEl,
- cb() {
- contentEl.scrollTo({ top: 0 })
+ scrollSpy,
+ cb: () => {
+ scrollTo(contentEl, 0)
testElementIsActiveAfterScroll({
elementSelector: '#li-100-4',
targetSelector: '#div-100-4',
- scrollSpy,
- spy,
contentEl,
- cb() {
- contentEl.scrollTo({ top: 0 })
+ scrollSpy,
+ cb: () => {
+ scrollTo(contentEl, 0)
testElementIsActiveAfterScroll({
elementSelector: '#li-100-3',
targetSelector: '#div-100-3',
- scrollSpy,
- spy,
contentEl,
- cb() {
- contentEl.scrollTo({ top: 0 })
+ scrollSpy,
+ cb: () => {
+ scrollTo(contentEl, 0)
testElementIsActiveAfterScroll({
elementSelector: '#li-100-2',
targetSelector: '#div-100-2',
- scrollSpy,
- spy,
contentEl,
- cb() {
- contentEl.scrollTo({ top: 0 })
+ scrollSpy,
+ cb: () => {
+ scrollTo(contentEl, 0)
testElementIsActiveAfterScroll({
elementSelector: '#li-100-1',
targetSelector: '#div-100-1',
- scrollSpy,
- spy,
contentEl,
+ scrollSpy,
cb: done
})
}