scroll to bottom logic

This commit is contained in:
Nate Kelley 2025-10-07 13:23:45 -06:00
parent b0fa427648
commit 037a5c640f
No known key found for this signature in database
GPG Key ID: FD90372AB8D98B4F
6 changed files with 60 additions and 6 deletions

View File

@ -2,10 +2,16 @@ import { Command } from 'cmdk';
import { cn } from '@/lib/utils';
import { IndeterminateLinearLoader } from '../../loaders';
export const SearchLoading = ({ loading = false }: { loading?: boolean }) => {
export const SearchLoading = ({
loading = false,
showTopLoading = false,
}: {
loading?: boolean;
showTopLoading?: boolean;
}) => {
return (
<Command.Loading className="w-full border-b swag relative">
{loading && (
{showTopLoading && loading && (
<IndeterminateLinearLoader
className={cn('w-full absolute top-0 left-0 right-0')}
height={0.5}

View File

@ -60,6 +60,7 @@ export const Default: Story = {
onChangeValue: fn(),
onSelect: fn(),
onViewSearchItem: fn(),
onScrollToBottom: fn(),
emptyState: 'No results found',
placeholder: 'Search for something',
filterContent: <div>Filter</div>,

View File

@ -24,7 +24,9 @@ export const SearchModalContent = <M, T extends string>({
secondaryContent,
openSecondaryContent,
shouldFilter = true,
showTopLoading = false,
filter,
onScrollToBottom,
}: SearchModalContentProps<M, T>) => {
const { handleKeyDown, focusedValue, setFocusedValue } = useViewSearchItem({
searchItems,
@ -71,14 +73,16 @@ export const SearchModalContent = <M, T extends string>({
open={open}
/>
<SearchLoading loading={loading} />
<SearchLoading loading={loading} showTopLoading={showTopLoading} />
<SearchModalItemsContainer
searchItems={searchItems}
secondaryContent={secondaryContent}
openSecondaryContent={openSecondaryContent}
loading={loading}
onSelectGlobal={onSelectGlobal}
onViewSearchItem={onViewSearchItem}
onScrollToBottom={onScrollToBottom}
/>
<SearchEmptyState emptyState={emptyState} />

View File

@ -18,16 +18,45 @@ type CommonProps<M, T extends string> = {
onSelectGlobal: (d: SearchItem<M, T>) => void;
};
const SCROLL_THRESHOLD = 55;
export const SearchModalContentItems = <M, T extends string>({
searchItems,
loading,
onSelectGlobal,
}: Pick<SearchModalContentProps<M, T>, 'searchItems' | 'onViewSearchItem'> & CommonProps<M, T>) => {
onScrollToBottom,
}: Pick<
SearchModalContentProps<M, T>,
'loading' | 'onScrollToBottom' | 'searchItems' | 'onViewSearchItem'
> &
CommonProps<M, T>) => {
const hasFiredRef = useRef(false);
const handleScroll = (e: React.UIEvent<HTMLDivElement>) => {
const target = e.currentTarget;
const scrollHeight = target.scrollHeight;
const scrollTop = target.scrollTop;
const clientHeight = target.clientHeight;
const distanceFromBottom = scrollHeight - scrollTop - clientHeight;
if (distanceFromBottom <= SCROLL_THRESHOLD) {
if (!hasFiredRef.current) {
hasFiredRef.current = true;
onScrollToBottom?.();
}
} else {
hasFiredRef.current = false;
}
};
return (
<Command.List
className={cn(
'flex flex-col overflow-y-auto flex-1 px-3 pt-1.5 pb-1.5',
'[&_[hidden]+[data-separator-after-hidden]]:hidden'
)}
onScroll={onScrollToBottom ? handleScroll : undefined}
>
{searchItems.map((item, index) => (
<ItemsSelecter
@ -36,6 +65,14 @@ export const SearchModalContentItems = <M, T extends string>({
onSelectGlobal={onSelectGlobal}
/>
))}
{loading && (
<div className="flex items-center justify-center my-1.5">
<Text size={'sm'} variant={'secondary'}>
Loading...
</Text>
</div>
)}
</Command.List>
);
};

View File

@ -1,8 +1,6 @@
import { useCommandState } from 'cmdk';
import { AnimatePresence, motion } from 'framer-motion';
import React from 'react';
import { cn } from '@/lib/utils';
import { SearchLoading } from './SearchLoading';
import { SearchModalContentItems } from './SearchModalContentItems';
import type { SearchItem, SearchItems, SearchModalContentProps } from './search-modal.types';
@ -14,12 +12,16 @@ export const SearchModalItemsContainer = <M, T extends string>({
onViewSearchItem,
secondaryContent,
openSecondaryContent,
loading,
onScrollToBottom,
}: {
searchItems: SearchItems<M, T>[];
loading: SearchModalContentProps<M, T>['loading'];
onSelectGlobal: (d: SearchItem<M, T>) => void;
onViewSearchItem: (item: SearchItem<M, T>) => void;
secondaryContent: SearchModalContentProps<M, T>['secondaryContent'];
openSecondaryContent: SearchModalContentProps<M, T>['openSecondaryContent'];
onScrollToBottom: SearchModalContentProps<M, T>['onScrollToBottom'];
}) => {
const hasResults = useCommandState((x) => x.filtered.count) > 0;
@ -33,8 +35,10 @@ export const SearchModalItemsContainer = <M, T extends string>({
>
<SearchModalContentItems
searchItems={searchItems}
loading={loading}
onSelectGlobal={onSelectGlobal}
onViewSearchItem={onViewSearchItem}
onScrollToBottom={onScrollToBottom}
/>
</motion.div>
<AnimatePresence>

View File

@ -43,8 +43,10 @@ export type SearchModalContentProps<M = unknown, T extends string = string> = {
emptyState?: React.ReactNode | string;
placeholder?: string;
loading?: boolean;
showTopLoading?: boolean;
secondaryContent?: React.ReactNode | null;
openSecondaryContent?: boolean; //if undefined it will close and open with the secondary content
onScrollToBottom?: () => void;
} & Pick<React.ComponentProps<typeof Command>, 'filter' | 'shouldFilter'>;
export type SearchModalProps = SearchModalContentProps & {