Skip to content

Commit 2d22277

Browse files
committed
bump 0.9.6, streamline hashnav, improve ff cancel
1 parent d47deea commit 2d22277

File tree

6 files changed

+68
-57
lines changed

6 files changed

+68
-57
lines changed

README.md

Lines changed: 38 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -16,14 +16,14 @@ But it may not be the one-size-fits-all solution to highlight menu/sidebar links
1616

1717
You may noticed that's tricky to customize behavior according to different interactions.
1818

19-
For example, you want to immediately highlight targets when scroll is originated from click/navigation but not when it is originated from wheel/touch. You want also to highlight any clicked link even if it will never intersect.
19+
For example, you want to immediately highlight targets when scroll is originated from click/navigation but not when it is originated from wheel/touch. You also want to highlight any clicked link even if it will never intersect.
2020

21-
**Vue Use Active Scroll** implements a custom scroll observer which automatically adapts to different interactions and always returns the "correct" active target.
21+
**Vue Use Active Scroll** implements a custom scroll observer which automatically adapts to any type of scroll behaviors and interactions and always returns the "correct" active target.
2222

2323
### Features
2424

2525
- Precise and stable at any speed
26-
- CSS scroll-behavior and callback agnostic
26+
- CSS scroll-behavior or JS scroll agnostic
2727
- Adaptive behavior on mount, back/forward hash navigation, scroll, click, cancel.
2828
- Customizable boundary offsets for each direction
2929
- Customizable behavior on top/bottom reached
@@ -69,7 +69,7 @@ And your links look like:
6969
</nav>
7070
```
7171

72-
In your menu/sidebar component, provide the IDs of the targets to observe to `useActive` (order is not
72+
In your menu/sidebar component, provide the IDs to observe to `useActive` (order is not
7373
important).
7474

7575
```vue
@@ -166,12 +166,12 @@ const { isActive, setActive } = useActive(targets, {
166166

167167
### Return object
168168

169-
| Name | Type | Description |
170-
| ----------- | ------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------- |
171-
| setActive | `(id: string) => void` | :firecracker: Function to include in your click handler to ensure adaptive behavior between any interaction that may cancel or resume scroll. |
172-
| isActive | `(id: string) => boolean` | Whether the given Id is active or not |
173-
| activeId | `Ref<string>` | Id of the active target |
174-
| activeIndex | `Ref<number>` | Index of the active target in offset order, `0` for the first target and so on. |
169+
| Name | Type | Description |
170+
| ----------- | ------------------------- | ------------------------------------------------------------------------------------ |
171+
| setActive | `(id: string) => void` | :firecracker: Function to include in your click handler to ensure adaptive behavior. |
172+
| isActive | `(id: string) => boolean` | Whether the given Id is active or not |
173+
| activeId | `Ref<string>` | Id of the active target |
174+
| activeIndex | `Ref<number>` | Index of the active target in offset order, `0` for the first target and so on. |
175175

176176
<br />
177177

@@ -230,7 +230,7 @@ html {
230230
}
231231
```
232232

233-
<details><summary><strong>B. Custom Scroll Animation</strong></summary>
233+
<details><summary><strong>B. Custom JS Scroll</strong></summary>
234234

235235
<br />
236236

@@ -353,7 +353,7 @@ html {
353353

354354
## Setting scroll-margin-top for fixed headers
355355

356-
You might noticed that if you have a fixed header and defined an `overlayHeight`, once you click to scroll to a target it may be underneath the header. You must set `scroll-margin-top` to your targets:
356+
You might noticed that if you have a fixed header and defined an `overlayHeight`, once you click to scroll to a target it may be underneath the header. You must add `scroll-margin-top` to your targets:
357357

358358
```js
359359
useActive(targets, { overlayHeight: 100 })
@@ -367,19 +367,25 @@ useActive(targets, { overlayHeight: 100 })
367367

368368
<br />
369369

370-
## Vue Router scroll to hash on mount
370+
## Vue Router Scroll Hash Navigation
371371

372-
If using Nuxt, Vue Router is already configured to scroll to the URL hash on page load or back/forward navigation.
372+
> :warning: If using Nuxt 3, Vue Router is already configured to scroll to and from URL hash on page load or back/forward navigation. **So you don't need to do follow the steps below**. Otherwise rules must be defined manually.
373373
374-
If not using Nuxt and you're setting up Vue Router from scratch, you must enable the feature manually.
374+
### Scrolling to hash
375375

376-
### Window
376+
For content scrolled by the window, simply return the target element. To scroll to a target scrolled by a container, use _scrollIntoView_ method.
377377

378378
```js
379379
const router = createRouter({
380380
// ...
381381
scrollBehavior(to) {
382382
if (to.hash) {
383+
// Content scrolled by a container
384+
if (to.name === 'PageNameUsingContainer') {
385+
return document.querySelector(to.hash).scrollIntoView()
386+
}
387+
388+
// Content scrolled by the window
383389
return {
384390
el: to.hash
385391
// top: 100 // Eventual fixed header (overlayHeight)
@@ -389,32 +395,34 @@ const router = createRouter({
389395
})
390396
```
391397

392-
> :bulb: There's need to define _smooth_ or _auto_ here. Adding the [CSS rule](#2-define-scroll-behavior) is enough. Same applies below.
398+
> :bulb: There's no need to define _smooth_ or _auto_ here. Adding the [CSS rule](#2-define-scroll-behavior) is enough.
393399
394-
### Container
400+
> :bulb: There's no need need to set overlayHeight if using `scrollIntoView` as the method is aware of target's `scroll-margin-top` property.
395401
396-
To scroll to a target scrolled by a container, you must use `scrollIntoView`:
402+
### Scrolling from hash to the top of the page
403+
404+
To navigate back to the top of the same page (e.g. clicking on browser back button from a hash to the page root), use the _scroll_ method for containers and return _top_ for content scrolled by the window.
397405

398406
```js
399407
const router = createRouter({
400408
// ...
401-
scrollBehavior(to) {
402-
if (to.hash) {
403-
// Content scrolled by container
404-
if (to.name === 'PageNameWithContainer') {
405-
return document.querySelector(to.hash).scrollIntoView()
406-
}
407-
// Content scrolled by window
408-
return {
409-
el: to.hash
409+
scrollBehavior(to, from) {
410+
if (from.hash && !to.hash) {
411+
// Content scrolled by a container
412+
if (
413+
to.name === 'PageNameUsingContainer' &&
414+
from.name === 'PageNameUsingContainer'
415+
) {
416+
return document.querySelector('#ScrollingContainer').scroll(0, 0)
410417
}
418+
419+
// Content scrolled by the window
420+
return { top: 0 }
411421
}
412422
}
413423
})
414424
```
415425

416-
> :bulb: No need to set overlayHeight if using `scrollIntoView` as the method is aware of target's `scroll-margin-top` property.
417-
418426
<br />
419427

420428
## License

demo/main.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,15 @@ const router = createRouter({
2626
component: () => import('./pages/Sections.vue'),
2727
},
2828
],
29-
scrollBehavior(to) {
29+
scrollBehavior(to, from) {
30+
if (from.hash && !to.hash) {
31+
if (to.name === 'Container' && from.name === 'Container') {
32+
return document.querySelector('#ScrollingContainer')?.scroll(0, 0);
33+
}
34+
35+
return { top: 0 };
36+
}
37+
3038
if (to.hash) {
3139
if (to.name === 'Container') {
3240
return document.querySelector(to.hash)?.scrollIntoView();

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "vue-use-active-scroll",
3-
"version": "0.9.4",
3+
"version": "0.9.6",
44
"private": false,
55
"description": "Reactive and accurate TOC/sidebar links without compromises for Vue 3.",
66
"keywords": [

src/useActive.ts

Lines changed: 7 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,10 @@ export function useActive(
114114
}
115115

116116
function onEdgeReached() {
117+
if (!jumpToFirst && !jumpToLast) {
118+
return false;
119+
}
120+
117121
const { isBottom, isTop } = getEdges(root.value as HTMLElement);
118122

119123
if (jumpToFirst && isTop) {
@@ -173,7 +177,7 @@ export function useActive(
173177
}
174178

175179
if (ids.value.indexOf(firstIn) < ids.value.indexOf(activeId.value)) {
176-
activeId.value = firstIn;
180+
return (activeId.value = firstIn);
177181
}
178182
}
179183

@@ -188,17 +192,9 @@ export function useActive(
188192

189193
function onHashChange(event: HashChangeEvent) {
190194
if (matchMedia.value) {
191-
// If hash is not anymore in the URL
195+
// If scrolled to top
192196
if (!event.newURL.includes('#') && activeId.value) {
193-
const prevY = getSentinel();
194-
195-
requestAnimationFrame(() => {
196-
// If scrolled to top on its own, reset activeId
197-
const newY = getSentinel();
198-
if (prevY !== newY) {
199-
return (activeId.value = jumpToFirst ? ids.value[0] : '');
200-
}
201-
});
197+
return (activeId.value = jumpToFirst ? ids.value[0] : '');
202198
}
203199

204200
// Else set hash as active

src/useScroll.ts

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { watch, onMounted, ref, computed, type Ref, type ComputedRef } from 'vue';
2-
import { isFirefox, isSSR, useMediaRef } from './utils';
2+
import { isSSR, useMediaRef } from './utils';
33

44
type UseScrollOptions = {
55
isWindow: ComputedRef<boolean>;
@@ -63,8 +63,10 @@ export function useScroll({
6363
const nextY = getY();
6464

6565
if (nextY < prevY) {
66+
console.log('Up');
6667
onScrollUp();
6768
} else {
69+
console.log('Down');
6870
onScrollDown({ isCancel });
6971
}
7072

@@ -87,7 +89,8 @@ export function useScroll({
8789
function onPointerDown(event: PointerEvent) {
8890
const isAnchor = (event.target as HTMLElement).tagName === 'A';
8991

90-
if (isFirefox() && !isAnchor) {
92+
if (CSS.supports('-moz-appearance', 'none') && !isAnchor) {
93+
console.log('PointerDown');
9194
reScroll();
9295
// ...and force set if canceling scroll
9396
setActive({ prevY: clickY.value, isCancel: true });
@@ -136,15 +139,17 @@ export function useScroll({
136139
const rootEl = isWindow.value ? document : root.value;
137140

138141
if (_isClick && rootEl) {
142+
console.log('Adding additional listeners');
139143
rootEl.addEventListener('wheel', reScroll, ONCE);
140144
rootEl.addEventListener('touchmove', reScroll, ONCE);
141145
rootEl.addEventListener('scroll', setIdle as unknown as EventListener, ONCE);
142146
rootEl.addEventListener('keydown', onSpaceBar as EventListener, ONCE);
143-
rootEl.addEventListener('pointerdown', onPointerDown as EventListener, ONCE);
147+
rootEl.addEventListener('pointerdown', onPointerDown as EventListener); // Must persist for proper cancel with Firefox
144148
}
145149

146150
onCleanup(() => {
147151
if (_isClick && rootEl) {
152+
console.log('Removing additional listeners');
148153
rootEl.removeEventListener('wheel', reScroll);
149154
rootEl.removeEventListener('touchmove', reScroll);
150155
rootEl.removeEventListener('scroll', setIdle as unknown as EventListener);

src/utils.ts

Lines changed: 5 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,6 @@ export const isSSR = typeof window === 'undefined';
44

55
export const FIXED_OFFSET = 5;
66

7-
export function isFirefox() {
8-
return CSS.supports('-moz-appearance', 'none');
9-
}
10-
117
// When users set refs, if no media match, set default value
128
export function useMediaRef<T>(matchMedia: Ref<boolean>, defaultValue: T): Ref<T> {
139
const _customRef = customRef<T>((track, trigger) => {
@@ -28,17 +24,15 @@ export function useMediaRef<T>(matchMedia: Ref<boolean>, defaultValue: T): Ref<T
2824
}
2925

3026
export function getEdges(root: HTMLElement) {
31-
// Mobile devices
27+
// Mobile devices needs window.innerHeight
3228
const clientHeight = root === document.documentElement ? window.innerHeight : root.clientHeight;
3329

34-
const isTopReached = root.scrollTop <= FIXED_OFFSET * 2;
35-
const isBottomReached = Math.abs(root.scrollHeight - clientHeight - root.scrollTop) <= 1;
36-
const isOverscrollTop = root.scrollTop < 0;
37-
const isOverscrollBottom = root.scrollTop > root.scrollHeight - clientHeight;
30+
const isTop = root.scrollTop <= FIXED_OFFSET * 2;
31+
const isBottom = Math.abs(root.scrollHeight - clientHeight - root.scrollTop) <= 1;
3832

3933
return {
40-
isTop: isTopReached || isOverscrollTop,
41-
isBottom: isBottomReached || isOverscrollBottom,
34+
isTop,
35+
isBottom,
4236
};
4337
}
4438

0 commit comments

Comments
 (0)