-
Notifications
You must be signed in to change notification settings - Fork 1.3k
feat: Async loading support for S2 ComboBox/Picker #7938
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
c4ca76d
8ee20f9
c2a1cf9
765b757
7708b81
2da7d04
0bb9f21
0049153
76e1a05
68033b2
54fcbaa
4d3bb6c
f6ba68a
3fe09f6
0b06824
da166f2
0ef52ef
51580d7
76dc08a
27260f4
41a28cb
4fb8aeb
f8b1745
b272469
7a2877c
4c5bc69
5c423d4
fdf423f
711dff6
286d8b8
0938fb9
1eeb798
5fc224f
18c65b2
85b7edb
7f57506
d493fc4
a69a794
4523888
3052001
6a98ac5
6fb2c01
97b21a2
e1bd8f3
d4bec98
7f062b8
561d914
dc6e80a
0c3a3b4
2085f8c
a9b7ea6
f60cbe3
0fa17cc
95beec7
fc193c5
cbd91b5
4a62993
f664da0
85440fc
09825f4
d98691f
f34fc0c
abfcde0
5258d8b
48caf94
379fba7
160e9a0
fac1920
f87438b
7c5f1e8
63db21e
1eb421e
15bc7d1
77b4b62
1bbd69c
bebc44b
9976091
9c51d0d
f7a1e20
53f9158
ebba44e
4755b91
5e8b6ae
e2b8c00
a0b9e5d
cb52f58
112df59
be7d233
69b7db1
3cbb286
923e260
a2049ab
65c2aca
01ead63
3ae8f29
32c3792
1680885
935e439
8bd923f
f4e5f0d
a69f298
67423b2
52e04bc
fb0aff3
f35df51
4637d34
b9e2ad7
875d9a3
9026acb
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,63 @@ | ||
/* | ||
* Copyright 2024 Adobe. All rights reserved. | ||
* This file is licensed to you under the Apache License, Version 2.0 (the "License"); | ||
* you may not use this file except in compliance with the License. You may obtain a copy | ||
* of the License at http://www.apache.org/licenses/LICENSE-2.0 | ||
* | ||
* Unless required by applicable law or agreed to in writing, software distributed under | ||
* the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS | ||
* OF ANY KIND, either express or implied. See the License for the specific language | ||
* governing permissions and limitations under the License. | ||
*/ | ||
|
||
import type {AsyncLoadable, Collection, Node} from '@react-types/shared'; | ||
import {getScrollParent} from './getScrollParent'; | ||
import {RefObject, useRef} from 'react'; | ||
import {useEffectEvent} from './useEffectEvent'; | ||
import {useLayoutEffect} from './useLayoutEffect'; | ||
|
||
export interface LoadMoreSentinelProps extends Omit<AsyncLoadable, 'isLoading'> { | ||
collection: Collection<Node<unknown>>, | ||
/** | ||
* The amount of offset from the bottom of your scrollable region that should trigger load more. | ||
* Uses a percentage value relative to the scroll body's client height. Load more is then triggered | ||
* when your current scroll position's distance from the bottom of the currently loaded list of items is less than | ||
* or equal to the provided value. (e.g. 1 = 100% of the scroll region's height). | ||
* @default 1 | ||
*/ | ||
scrollOffset?: number | ||
} | ||
|
||
export function UNSTABLE_useLoadMoreSentinel(props: LoadMoreSentinelProps, ref: RefObject<HTMLElement | null>): void { | ||
let {collection, onLoadMore, scrollOffset = 1} = props; | ||
|
||
let sentinelObserver = useRef<IntersectionObserver>(null); | ||
|
||
let triggerLoadMore = useEffectEvent((entries: IntersectionObserverEntry[]) => { | ||
// Use "isIntersecting" over an equality check of 0 since it seems like there is cases where | ||
// a intersection ratio of 0 can be reported when isIntersecting is actually true | ||
for (let entry of entries) { | ||
// Note that this will be called if the collection changes, even if onLoadMore was already called and is being processed. | ||
// Up to user discretion as to how to handle these multiple onLoadMore calls | ||
if (entry.isIntersecting && onLoadMore) { | ||
onLoadMore(); | ||
} | ||
} | ||
}); | ||
|
||
useLayoutEffect(() => { | ||
if (ref.current) { | ||
// Tear down and set up a new IntersectionObserver when the collection changes so that we can properly trigger additional loadMores if there is room for more items | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. seems fine, certainly more readable than separating it into some other effect and has the benefit of being queued in the same way as the rest of the IntersectionObserver updates |
||
// Need to do this tear down and set up since using a large rootMargin will mean the observer's callback isn't called even when scrolling the item into view beause its visibility hasn't actually changed | ||
// https://codesandbox.io/p/sandbox/magical-swanson-dhgp89?file=%2Fsrc%2FApp.js%3A21%2C21 | ||
sentinelObserver.current = new IntersectionObserver(triggerLoadMore, {root: getScrollParent(ref?.current) as HTMLElement, rootMargin: `0px ${100 * scrollOffset}% ${100 * scrollOffset}% ${100 * scrollOffset}%`}); | ||
sentinelObserver.current.observe(ref.current); | ||
} | ||
|
||
return () => { | ||
if (sentinelObserver.current) { | ||
sentinelObserver.current.disconnect(); | ||
} | ||
}; | ||
}, [collection, triggerLoadMore, ref, scrollOffset]); | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -98,7 +98,6 @@ export function useScrollView(props: ScrollViewProps, ref: RefObject<HTMLElement | |
// Prevent rubber band scrolling from shaking when scrolling out of bounds | ||
state.scrollTop = Math.max(0, Math.min(scrollTop, contentSize.height - state.height)); | ||
state.scrollLeft = Math.max(0, Math.min(scrollLeft, contentSize.width - state.width)); | ||
|
||
onVisibleRectChange(new Rect(state.scrollLeft, state.scrollTop, state.width, state.height)); | ||
|
||
if (!state.isScrolling) { | ||
|
@@ -199,6 +198,7 @@ export function useScrollView(props: ScrollViewProps, ref: RefObject<HTMLElement | |
|
||
// Update visible rect when the content size changes, in case scrollbars need to appear or disappear. | ||
let lastContentSize = useRef<Size | null>(null); | ||
let [update, setUpdate] = useState({}); | ||
useLayoutEffect(() => { | ||
if (!isUpdatingSize.current && (lastContentSize.current == null || !contentSize.equals(lastContentSize.current))) { | ||
// React doesn't allow flushSync inside effects, so queue a microtask. | ||
|
@@ -209,7 +209,11 @@ export function useScrollView(props: ScrollViewProps, ref: RefObject<HTMLElement | |
// https://github.com/reactwg/react-18/discussions/102 | ||
// @ts-ignore | ||
if (typeof IS_REACT_ACT_ENVIRONMENT === 'boolean' ? IS_REACT_ACT_ENVIRONMENT : typeof jest !== 'undefined') { | ||
updateSize(fn => fn()); | ||
// This is so we update size in a separate render but within the same act. Needs to be setState instead of refs | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. why? is there some event we could hook into instead? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is to specifically fix the tests where the order in which things run (such as when the ref is connected to the ScrollView and when we perform rect measurements) vs how focus is moved by simulated clicks/presses from userEvent was causing odd behaviors (i.e. focus would move to the trigger button -> the dropdown -> back to the button specifically due to the order in which the dropdown becomes available/focusable + The only way to fix this was to make sure the size of the ScrollView updates in a separate render but within the same act, but ideally the high level flow should be that focus moves to the Picker trigger on click -> all press handling finishes -> collection is fully formed -> dropdown opens and focus moves into it. |
||
// due to strict mode. | ||
setUpdate({}); | ||
lastContentSize.current = contentSize; | ||
return; | ||
} else { | ||
queueMicrotask(() => updateSize(flushSync)); | ||
} | ||
|
@@ -218,6 +222,11 @@ export function useScrollView(props: ScrollViewProps, ref: RefObject<HTMLElement | |
lastContentSize.current = contentSize; | ||
}); | ||
|
||
// Will only run in tests, needs to be in separate effect so it is properly run in the next render in strict mode. | ||
useLayoutEffect(() => { | ||
updateSize(fn => fn()); | ||
}, [update]); | ||
|
||
let onResize = useCallback(() => { | ||
updateSize(flushSync); | ||
}, [updateSize]); | ||
|
Uh oh!
There was an error while loading. Please reload this page.