Skip to content

Commit

Permalink
ScrollSpy: improve active feedback
Browse files Browse the repository at this point in the history
  • Loading branch information
guilhas07 committed Dec 20, 2024
1 parent 0cbfe13 commit 17dc58b
Show file tree
Hide file tree
Showing 2 changed files with 24 additions and 45 deletions.
62 changes: 20 additions & 42 deletions js/src/scrollspy.js
Original file line number Diff line number Diff line change
Expand Up @@ -40,10 +40,10 @@ const SELECTOR_DROPDOWN_TOGGLE = '.dropdown-toggle'

const Default = {
offset: null, // TODO: v6 @deprecated, keep it for backwards compatibility reasons
rootMargin: '0px 0px -25%',
rootMargin: '0px',
smoothScroll: false,
target: null,
threshold: [0.1, 0.5, 1]
threshold: [0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1]
}

const DefaultType = {
Expand All @@ -65,13 +65,10 @@ class ScrollSpy extends BaseComponent {
// this._element is the observablesContainer and config.target the menu links wrapper
this._targetLinks = new Map()
this._observableSections = new Map()
this._intersectionRatio = new Map()
this._rootElement = getComputedStyle(this._element).overflowY === 'visible' ? null : this._element
this._activeTarget = null
this._observer = null
this._previousScrollData = {
visibleEntryTop: 0,
parentScrollTop: 0
}
this.refresh() // initialize
}

Expand Down Expand Up @@ -115,7 +112,7 @@ class ScrollSpy extends BaseComponent {
config.target = getElement(config.target) || document.body

// TODO: v6 Only for backwards compatibility reasons. Use rootMargin only
config.rootMargin = config.offset ? `${config.offset}px 0px -30%` : config.rootMargin
config.rootMargin = config.offset ? `${config.offset}px 0px 0px` : config.rootMargin

if (typeof config.threshold === 'string') {
config.threshold = config.threshold.split(',').map(value => Number.parseFloat(value))
Expand Down Expand Up @@ -161,40 +158,26 @@ class ScrollSpy extends BaseComponent {

// The logic of selection
_observerCallback(entries) {
const targetElement = entry => this._targetLinks.get(`#${entry.target.id}`)
const activate = entry => {
this._previousScrollData.visibleEntryTop = entry.target.offsetTop
this._process(targetElement(entry))
}
const targetElement = entryId => this._targetLinks.get(entryId)

const parentScrollTop = (this._rootElement || document.documentElement).scrollTop
const userScrollsDown = parentScrollTop >= this._previousScrollData.parentScrollTop
this._previousScrollData.parentScrollTop = parentScrollTop
// always remove active from `target` children before each cycle
this._clearActiveClass(this._config.target)

for (const entry of entries) {
if (!entry.isIntersecting) {
this._activeTarget = null
this._clearActiveClass(targetElement(entry))
this._intersectionRatio.set(`#${entry.target.id}`, entry.intersectionRatio)
}

continue
}

const entryIsLowerThanPrevious = entry.target.offsetTop >= this._previousScrollData.visibleEntryTop
// if we are scrolling down, pick the bigger offsetTop
if (userScrollsDown && entryIsLowerThanPrevious) {
activate(entry)
// if parent isn't scrolled, let's keep the first visible item, breaking the iteration
if (!parentScrollTop) {
return
}

continue
let maxIntersectionRatio = 0
for (const [key, val] of this._intersectionRatio.entries()) {
if (val > maxIntersectionRatio) {
const element = targetElement(key)
this._activeTarget = element
maxIntersectionRatio = val
}
}

// if we are scrolling up, pick the smallest offsetTop
if (!userScrollsDown && !entryIsLowerThanPrevious) {
activate(entry)
}
if (this._activeTarget !== null) {
this._process(this._activeTarget)
}
}

Expand All @@ -216,20 +199,15 @@ class ScrollSpy extends BaseComponent {
if (isVisible(observableSection)) {
this._targetLinks.set(decodeURI(anchor.hash), anchor)
this._observableSections.set(anchor.hash, observableSection)
this._intersectionRatio.set(anchor.hash, 0)
}
}
}

_process(target) {
if (this._activeTarget === target) {
return
}

this._clearActiveClass(this._config.target)
this._activeTarget = target
target.classList.add(CLASS_NAME_ACTIVE)
this._activateParents(target)

target.classList.add(CLASS_NAME_ACTIVE)
EventHandler.trigger(this._element, EVENT_ACTIVATE, { relatedTarget: target })
}

Expand Down
7 changes: 4 additions & 3 deletions js/tests/unit/scrollspy.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -451,7 +451,7 @@ describe('ScrollSpy', () => {
})
})

it('should clear selection if above the first section', () => {
it('should remember previous selection', () => {
return new Promise(resolve => {
fixtureEl.innerHTML = [
'<div id="header" style="height: 500px;"></div>',
Expand Down Expand Up @@ -483,9 +483,10 @@ describe('ScrollSpy', () => {
expect(spy).toHaveBeenCalled()

expect(fixtureEl.querySelectorAll('.active')).toHaveSize(1)
expect(active().getAttribute('id')).toEqual('two-link')
expect(active().getAttribute('id')).toEqual('one-link')
onScrollStop(() => {
expect(active()).toBeNull()
expect(fixtureEl.querySelectorAll('.active')).toHaveSize(1)
expect(active().getAttribute('id')).toEqual('one-link')
resolve()
}, contentEl)
scrollTo(contentEl, 0)
Expand Down

0 comments on commit 17dc58b

Please sign in to comment.