Skip to content

Commit 008438a

Browse files
committed
moved scroll funcs to useScroll, streamline return
1 parent 53d83fd commit 008438a

File tree

4 files changed

+94
-87
lines changed

4 files changed

+94
-87
lines changed

.eslintrc.json

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,12 @@
1919
"curly": ["error", "all"],
2020
"no-useless-return": "error",
2121
"no-else-return": "error",
22-
"nonblock-statement-body-position": ["error", "below"]
22+
"nonblock-statement-body-position": ["error", "below"],
23+
"@typescript-eslint/ban-ts-comment": [
24+
"error",
25+
{
26+
"ts-ignore": "allow-with-description"
27+
}
28+
]
2329
}
2430
}

src/useActive.ts

Lines changed: 30 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import {
1313
import { useScroll } from './useScroll';
1414
import { getEdges, useMediaRef, isSSR, FIXED_OFFSET, type DeepNonNullable } from './utils';
1515

16-
type UseActiveTitleOptions = {
16+
type UseActiveOptions = {
1717
jumpToFirst?: boolean;
1818
jumpToLast?: boolean;
1919
overlayHeight?: number;
@@ -26,14 +26,14 @@ type UseActiveTitleOptions = {
2626
};
2727
};
2828

29-
type UseActiveTitleReturn = {
29+
type UseActiveReturn = {
3030
isActive: (id: string) => boolean;
3131
setActive: (id: string) => void;
3232
activeId: Ref<string>;
3333
activeIndex: Ref<number>;
3434
};
3535

36-
const defaultOpts: DeepNonNullable<UseActiveTitleOptions> = {
36+
const defaultOpts: DeepNonNullable<UseActiveOptions> = {
3737
jumpToFirst: true,
3838
jumpToLast: true,
3939
overlayHeight: 0,
@@ -43,8 +43,7 @@ const defaultOpts: DeepNonNullable<UseActiveTitleOptions> = {
4343
toTop: 0,
4444
toBottom: 0,
4545
},
46-
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
47-
// @ts-ignore
46+
// @ts-ignore - Internal
4847
rootId: null,
4948
};
5049

@@ -61,10 +60,12 @@ export function useActive(
6160
toTop = defaultOpts.boundaryOffset.toTop,
6261
toBottom = defaultOpts.boundaryOffset.toTop,
6362
} = defaultOpts.boundaryOffset,
64-
}: UseActiveTitleOptions = defaultOpts
65-
): UseActiveTitleReturn {
63+
}: UseActiveOptions = defaultOpts
64+
): UseActiveReturn {
6665
const media = `(min-width: ${minWidth}px)`;
6766

67+
// Reactivity
68+
6869
// Internal
6970
const matchMedia = ref(isSSR || window.matchMedia(media).matches);
7071
const root = ref<HTMLElement | null>(null);
@@ -77,10 +78,12 @@ export function useActive(
7778
const isWindow = computed(() => root.value === document.documentElement);
7879
const ids = computed(() => targets.elements.map(({ id }) => id));
7980

80-
// Returned values
81+
// Returned
8182
const activeId = useMediaRef(matchMedia, '');
8283
const activeIndex = computed(() => ids.value.indexOf(activeId.value));
8384

85+
// Functions
86+
8487
function getTop() {
8588
if (root.value) {
8689
return root.value.getBoundingClientRect().top - (isWindow.value ? 0 : root.value.scrollTop);
@@ -110,7 +113,7 @@ export function useActive(
110113
});
111114
}
112115

113-
function jumpToEdges() {
116+
function onEdgeReached() {
114117
const { isBottom, isTop } = getEdges(root.value as HTMLElement);
115118

116119
if (jumpToFirst && isTop) {
@@ -121,20 +124,6 @@ export function useActive(
121124
}
122125
}
123126

124-
function _setActive(prevY: number, { isCancel } = { isCancel: false }) {
125-
const nextY = isWindow.value ? window.scrollY : (root.value as HTMLElement).scrollTop;
126-
127-
if (nextY < prevY) {
128-
onScrollUp();
129-
} else {
130-
onScrollDown({ isCancel });
131-
}
132-
133-
if (!isCancel) {
134-
jumpToEdges();
135-
}
136-
}
137-
138127
function getSentinel() {
139128
return isWindow.value ? getTop() : -(root.value as HTMLElement).scrollTop;
140129
}
@@ -193,6 +182,8 @@ export function useActive(
193182
setTargets();
194183
}
195184

185+
// Lifecycle
186+
196187
onMounted(async () => {
197188
root.value = rootId
198189
? document.getElementById(rootId) ?? document.documentElement
@@ -211,7 +202,7 @@ export function useActive(
211202
return (activeId.value = hashId);
212203
}
213204

214-
if (!jumpToEdges()) {
205+
if (!onEdgeReached()) {
215206
onScrollDown();
216207
}
217208
}
@@ -221,12 +212,7 @@ export function useActive(
221212
window.removeEventListener('resize', onResize);
222213
});
223214

224-
const isClick = useScroll({
225-
isWindow,
226-
root,
227-
matchMedia,
228-
_setActive,
229-
});
215+
// Watchers
230216

231217
watch(isRef(userIds) || isReactive(userIds) ? userIds : () => null, setTargets, {
232218
flush: 'post',
@@ -248,22 +234,23 @@ export function useActive(
248234
}
249235
});
250236

251-
// Returned
252-
function setActive(id: string) {
253-
if (id !== activeId.value) {
254-
activeId.value = id;
255-
isClick.value = true;
256-
}
257-
}
237+
// Composables
258238

259-
// Returned
260-
function isActive(id: string) {
261-
return id === activeId.value;
262-
}
239+
const setClick = useScroll({
240+
isWindow,
241+
root,
242+
matchMedia,
243+
onScrollUp,
244+
onScrollDown,
245+
onEdgeReached,
246+
});
263247

264248
return {
265-
isActive,
266-
setActive,
249+
isActive: (id) => id === activeId.value,
250+
setActive: (id) => {
251+
activeId.value = id;
252+
setClick();
253+
},
267254
activeId,
268255
activeIndex,
269256
};

src/useScroll.ts

Lines changed: 56 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -1,42 +1,54 @@
11
import { watch, onMounted, ref, computed, type Ref, type ComputedRef } from 'vue';
22
import { isChromium, isSSR, useMediaRef } from './utils';
33

4-
type UseListenersOptions = {
4+
type UseScrollOptions = {
55
isWindow: ComputedRef<boolean>;
66
root: Ref<HTMLElement | null>;
77
matchMedia: Ref<boolean>;
8-
_setActive: (prevY: number, isCancel?: { isCancel: boolean }) => void;
8+
onScrollUp: () => void;
9+
onScrollDown: ({ isCancel }: { isCancel: boolean }) => void;
10+
onEdgeReached: () => void;
911
};
1012

1113
const ONCE = { once: true };
1214

13-
export function useScroll({ isWindow, root, _setActive, matchMedia }: UseListenersOptions) {
15+
export function useScroll({
16+
isWindow,
17+
root,
18+
matchMedia,
19+
onScrollUp,
20+
onScrollDown,
21+
onEdgeReached,
22+
}: UseScrollOptions) {
1423
const isClick = useMediaRef(matchMedia, false);
15-
const isReady = ref(false);
16-
const clickY = computed(() => (isClick.value ? getNextY() : 0));
24+
const isIdle = ref(false);
25+
const clickY = computed(() => (isClick.value ? getY() : 0));
1726

18-
let prevY: number;
27+
let prevY = getY();
1928

20-
function getNextY() {
21-
return isWindow.value ? window.scrollY : root.value?.scrollTop || 0;
29+
function getY() {
30+
return isWindow.value ? window.scrollY : root.value?.scrollTop || 0; // SSR safe
2231
}
2332

24-
function setReady(maxFrames: number) {
33+
function setIdle(maxFrames = 20) {
2534
let rafId: DOMHighResTimeStamp | undefined = undefined;
26-
let rafPrevY: number;
35+
let rafPrevY = getY();
2736
let frameCount = 0;
2837

2938
function scrollEnd() {
30-
const rafNextY = getNextY();
31-
if (typeof rafPrevY === 'undefined' || rafPrevY !== rafNextY) {
39+
frameCount++;
40+
41+
const rafNextY = getY();
42+
43+
if (rafPrevY !== rafNextY) {
3244
frameCount = 0;
3345
rafPrevY = rafNextY;
3446
return requestAnimationFrame(scrollEnd);
3547
}
48+
3649
// When equal, wait for n frames after scroll to make sure is idle
37-
frameCount++;
3850
if (frameCount === maxFrames) {
39-
isReady.value = true;
51+
isIdle.value = true;
4052
isClick.value = false;
4153
cancelAnimationFrame(rafId as DOMHighResTimeStamp);
4254
} else {
@@ -47,15 +59,23 @@ export function useScroll({ isWindow, root, _setActive, matchMedia }: UseListene
4759
rafId = requestAnimationFrame(scrollEnd);
4860
}
4961

62+
function setActive({ prevY, isCancel = false }: { prevY: number; isCancel?: boolean }) {
63+
const nextY = getY();
64+
65+
if (nextY < prevY) {
66+
onScrollUp();
67+
} else {
68+
onScrollDown({ isCancel });
69+
}
70+
71+
return nextY;
72+
}
73+
5074
function onScroll() {
5175
// Do not "update" results if scrolling from click
5276
if (!isClick.value) {
53-
const nextY = getNextY();
54-
if (!prevY) {
55-
prevY = nextY;
56-
}
57-
_setActive(prevY);
58-
prevY = nextY;
77+
prevY = setActive({ prevY });
78+
onEdgeReached();
5979
}
6080
}
6181

@@ -64,45 +84,41 @@ export function useScroll({ isWindow, root, _setActive, matchMedia }: UseListene
6484
isClick.value = false;
6585
}
6686

67-
function onSpaceBar(event: KeyboardEvent) {
68-
if (event.code === 'Space') {
69-
reScroll();
70-
}
71-
}
72-
73-
function waitForIdle() {
74-
setReady(20);
75-
}
76-
7787
function onPointerDown(event: PointerEvent) {
7888
const isLink = (event.target as HTMLElement).tagName === 'A';
7989
const hasLink = (event.target as HTMLElement).closest('a');
8090

8191
if (!isChromium && !isLink && !hasLink) {
8292
reScroll();
8393
// ...and force set if canceling scroll
84-
_setActive(clickY.value, { isCancel: true });
94+
setActive({ prevY: clickY.value, isCancel: true });
95+
}
96+
}
97+
98+
function onSpaceBar(event: KeyboardEvent) {
99+
if (event.code === 'Space') {
100+
reScroll();
85101
}
86102
}
87103

88104
onMounted(() => {
89105
if (matchMedia.value && location.hash) {
90106
// Wait for any eventual scroll to hash triggered by browser to end
91-
setReady(10);
107+
setIdle(10);
92108
} else {
93-
isReady.value = true;
109+
isIdle.value = true;
94110
}
95111
});
96112

97113
watch(
98-
[isReady, matchMedia, root],
99-
([_isReady, _matchMedia, _root], _, onCleanup) => {
114+
[isIdle, matchMedia, root],
115+
([_isIdle, _matchMedia, _root], _, onCleanup) => {
100116
if (isSSR) {
101117
return;
102118
}
103119

104120
const rootEl = isWindow.value ? document : _root;
105-
const isActive = rootEl && _isReady && _matchMedia;
121+
const isActive = rootEl && _isIdle && _matchMedia;
106122

107123
if (isActive) {
108124
rootEl.addEventListener('scroll', onScroll, {
@@ -116,7 +132,7 @@ export function useScroll({ isWindow, root, _setActive, matchMedia }: UseListene
116132
}
117133
});
118134
},
119-
{ immediate: true, flush: 'sync' }
135+
{ flush: 'sync' }
120136
);
121137

122138
watch(
@@ -125,18 +141,18 @@ export function useScroll({ isWindow, root, _setActive, matchMedia }: UseListene
125141
const rootEl = isWindow.value ? document : root.value;
126142

127143
if (_isClick && rootEl) {
128-
rootEl.addEventListener('scroll', waitForIdle, ONCE);
129144
rootEl.addEventListener('wheel', reScroll, ONCE);
130145
rootEl.addEventListener('touchmove', reScroll, ONCE);
146+
rootEl.addEventListener('scroll', setIdle as unknown as EventListener, ONCE);
131147
rootEl.addEventListener('keydown', onSpaceBar as EventListener, ONCE);
132148
rootEl.addEventListener('pointerdown', onPointerDown as EventListener, ONCE);
133149
}
134150

135151
onCleanup(() => {
136152
if (_isClick && rootEl) {
137-
rootEl.removeEventListener('scroll', waitForIdle);
138153
rootEl.removeEventListener('wheel', reScroll);
139154
rootEl.removeEventListener('touchmove', reScroll);
155+
rootEl.removeEventListener('scroll', setIdle as unknown as EventListener);
140156
rootEl.removeEventListener('keydown', onSpaceBar as EventListener);
141157
rootEl.removeEventListener('pointerdown', onPointerDown as EventListener);
142158
}
@@ -145,5 +161,5 @@ export function useScroll({ isWindow, root, _setActive, matchMedia }: UseListene
145161
{ flush: 'sync' }
146162
);
147163

148-
return isClick;
164+
return () => (isClick.value = true);
149165
}

src/utils.ts

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -42,9 +42,7 @@ export function getEdges(root: HTMLElement) {
4242
}
4343

4444
// https://github.com/esamattis/utils/blob/master/src/DeepRequired.ts
45-
type Primitive = undefined | null | boolean | string | number;
46-
47-
export type DeepNonNullable<T> = T extends Primitive
45+
export type DeepNonNullable<T> = T extends undefined | null | boolean | string | number
4846
? NonNullable<T>
4947
: {
5048
[P in keyof T]-?: T[P] extends Array<infer U>

0 commit comments

Comments
 (0)