pl-fe: try to improve hotkeys scroll, reuse code

Signed-off-by: nicole mikołajczyk <git@mkljczk.pl>
This commit is contained in:
nicole mikołajczyk
2025-09-17 22:03:01 +02:00
parent 2399266e54
commit 96698cc801
7 changed files with 45 additions and 108 deletions

View File

@ -23,6 +23,7 @@ import { useAppDispatch } from 'pl-fe/hooks/use-app-dispatch';
import { useAppSelector } from 'pl-fe/hooks/use-app-selector';
import { useFeatures } from 'pl-fe/hooks/use-features';
import { useSettings } from 'pl-fe/hooks/use-settings';
import { selectChild } from 'pl-fe/utils/scroll-utils';
import type { Item } from 'pl-fe/components/ui/tabs';
import type { RootState } from 'pl-fe/store';
@ -182,27 +183,12 @@ const NotificationsColumn = () => {
const handleMoveUp = (id: string) => {
const elementIndex = displayedNotifications.findIndex(item => item !== null && item.group_key === id) - 1;
_selectChild(elementIndex);
selectChild(elementIndex, node);
};
const handleMoveDown = (id: string) => {
const elementIndex = displayedNotifications.findIndex(item => item !== null && item.group_key === id) + 1;
_selectChild(elementIndex);
};
const _selectChild = (index: number) => {
const selector = `[data-index="${index}"] .focusable`;
const element = document.querySelector<HTMLDivElement>(selector);
if (element) element.focus();
node.current?.scrollIntoView({
index,
behavior: 'smooth',
done: () => {
if (!element) document.querySelector<HTMLDivElement>(selector)?.focus();
},
});
selectChild(elementIndex, node);
};
const handleDequeueNotifications = useCallback(() => {

View File

@ -10,6 +10,7 @@ import PlaceholderAccount from 'pl-fe/features/placeholder/components/placeholde
import PlaceholderHashtag from 'pl-fe/features/placeholder/components/placeholder-hashtag';
import PlaceholderStatus from 'pl-fe/features/placeholder/components/placeholder-status';
import { useSearchAccounts, useSearchHashtags, useSearchStatuses } from 'pl-fe/queries/search/use-search';
import { selectChild } from 'pl-fe/utils/scroll-utils';
import TrendsColumn from './trends';
@ -46,29 +47,14 @@ const SearchColumn: React.FC<ISearchColumn> = ({ type, query, accountId, multiCo
if (!resultsIds) return;
const elementIndex = getCurrentIndex(id) - 1;
selectChild(elementIndex);
selectChild(elementIndex, node, document.getElementById('search-results') || undefined);
};
const handleMoveDown = (id: string) => {
if (!resultsIds) return;
const elementIndex = getCurrentIndex(id) + 1;
selectChild(elementIndex);
};
const selectChild = (index: number) => {
const selector = `#search-results [data-index="${index}"] .focusable`;
const element = document.querySelector<HTMLDivElement>(selector);
if (element) element.focus();
node.current?.scrollIntoView({
index,
behavior: 'smooth',
done: () => {
if (!element) document.querySelector<HTMLDivElement>(selector)?.focus();
},
});
selectChild(elementIndex, node, document.getElementById('search-results') || undefined);
};
const handleLoadMore = () => activeQuery.fetchNextPage({ cancelRefetch: false });

View File

@ -10,6 +10,7 @@ import Text from 'pl-fe/components/ui/text';
import StatusContainer from 'pl-fe/containers/status-container';
import PlaceholderStatus from 'pl-fe/features/placeholder/components/placeholder-status';
import PendingStatus from 'pl-fe/features/ui/components/pending-status';
import { selectChild } from 'pl-fe/utils/scroll-utils';
import type { VirtuosoHandle } from 'react-virtuoso';
@ -65,12 +66,12 @@ const StatusList: React.FC<IStatusList> = ({
const handleMoveUp = (id: string, featured: boolean = false) => {
const elementIndex = getCurrentStatusIndex(id, featured) - 1;
selectChild(elementIndex);
selectChild(elementIndex, node, document.getElementById('status-list') || undefined);
};
const handleMoveDown = (id: string, featured: boolean = false) => {
const elementIndex = getCurrentStatusIndex(id, featured) + 1;
selectChild(elementIndex);
selectChild(elementIndex, node, document.getElementById('status-list') || undefined);
};
const handleLoadOlder = useCallback(debounce(() => {
@ -80,21 +81,6 @@ const StatusList: React.FC<IStatusList> = ({
}
}, 300, { leading: true }), [onLoadMore, lastStatusId, statusIds.at(-1)]);
const selectChild = (index: number) => {
const selector = `#status-list [data-index="${index}"] .focusable`;
const element = document.querySelector<HTMLDivElement>(selector);
if (element) element.focus();
node.current?.scrollIntoView({
index,
behavior: 'smooth',
done: () => {
if (!element) document.querySelector<HTMLDivElement>(selector)?.focus();
},
});
};
const renderLoadGap = (index: number) => {
const ids = statusIds;
const nextId = ids[index + 1];

View File

@ -6,6 +6,7 @@ import { expandConversations } from 'pl-fe/actions/conversations';
import ScrollableList from 'pl-fe/components/scrollable-list';
import { useAppDispatch } from 'pl-fe/hooks/use-app-dispatch';
import { useAppSelector } from 'pl-fe/hooks/use-app-selector';
import { selectChild } from 'pl-fe/utils/scroll-utils';
import Conversation from './conversation';
@ -23,27 +24,12 @@ const ConversationsList: React.FC = () => {
const handleMoveUp = (id: string) => {
const elementIndex = getCurrentIndex(id) - 1;
selectChild(elementIndex);
selectChild(elementIndex, ref, document.getElementById('direct-list') || undefined);
};
const handleMoveDown = (id: string) => {
const elementIndex = getCurrentIndex(id) + 1;
selectChild(elementIndex);
};
const selectChild = (index: number) => {
const selector = `#direct-list [data-index="${index}"] .focusable`;
const element = document.querySelector<HTMLDivElement>(selector);
if (element) element.focus();
ref.current?.scrollIntoView({
index,
behavior: 'smooth',
done: () => {
if (!element) document.querySelector<HTMLDivElement>(selector)?.focus();
},
});
selectChild(elementIndex, ref, document.getElementById('direct-list') || undefined);
};
const handleLoadOlder = debounce(() => {

View File

@ -20,6 +20,7 @@ import { RootState } from 'pl-fe/store';
import { useModalsStore } from 'pl-fe/stores/modals';
import { useSettingsStore } from 'pl-fe/stores/settings';
import { useStatusMetaStore } from 'pl-fe/stores/status-meta';
import { selectChild } from 'pl-fe/utils/scroll-utils';
import { textForScreenReader } from 'pl-fe/utils/status';
import DetailedStatus from './detailed-status';
@ -241,52 +242,37 @@ const Thread = ({
};
const handleMoveUp = (id: string) => {
const modalOffset = isModal ? 1 : 0;
if (id === status.id) {
_selectChild(statusIndex - 1);
selectChild(statusIndex - 1 + modalOffset, scroller, node.current || undefined);
} else {
let index = thread.indexOf(id);
if (index === -1) {
index = thread.indexOf(id);
_selectChild(index);
selectChild(index + modalOffset, scroller, node.current || undefined);
} else {
_selectChild(index - 1);
selectChild(index - 1 + modalOffset, scroller, node.current || undefined);
}
}
};
const handleMoveDown = (id: string) => {
const modalOffset = isModal ? 1 : 0;
if (id === status.id) {
_selectChild(statusIndex + 1);
selectChild(statusIndex + 1 + modalOffset, scroller, node.current || undefined);
} else {
let index = thread.indexOf(id);
if (index === -1) {
index = thread.indexOf(id);
_selectChild(index);
selectChild(index + modalOffset, scroller, node.current || undefined);
} else {
_selectChild(index + 1);
selectChild(index + 1 + modalOffset, scroller, node.current || undefined);
}
}
};
const _selectChild = (index: number) => {
if (isModal) index = index + 1;
const selector = `[data-index="${index}"] .focusable`;
const element = node.current?.querySelector<HTMLDivElement>(selector);
if (element) element.focus();
scroller.current?.scrollIntoView({
index,
behavior: 'smooth',
done: () => {
if (!element) node.current?.querySelector<HTMLDivElement>(selector)?.focus();
},
});
};
const renderTombstone = (id: string) => (
<div className='py-4 pb-8'>
<Tombstone

View File

@ -15,6 +15,7 @@ import { ComposeForm } from 'pl-fe/features/ui/util/async-components';
import { useAppDispatch } from 'pl-fe/hooks/use-app-dispatch';
import { useAppSelector } from 'pl-fe/hooks/use-app-selector';
import { makeGetStatus } from 'pl-fe/selectors';
import { selectChild } from 'pl-fe/utils/scroll-utils';
import type { MediaAttachment } from 'pl-api';
import type { VirtuosoHandle } from 'react-virtuoso';
@ -60,27 +61,12 @@ const EventDiscussionPage: React.FC<IEventDiscussion> = ({ params: { statusId: s
const handleMoveUp = (id: string) => {
const index = descendantsIds.indexOf(id);
_selectChild(index - 1);
selectChild(index - 1, scroller, node.current || undefined);
};
const handleMoveDown = (id: string) => {
const index = descendantsIds.indexOf(id);
_selectChild(index + 1);
};
const _selectChild = (index: number) => {
const selector = `#thread [data-index="${index}"] .focusable`;
const element = document.querySelector<HTMLDivElement>(selector);
if (element) element.focus();
scroller.current?.scrollIntoView({
index,
behavior: 'smooth',
done: () => {
if (!element) document.querySelector<HTMLDivElement>(selector)?.focus();
},
});
selectChild(index + 1, scroller, node.current || undefined);
};
const renderTombstone = (id: string) => (

View File

@ -0,0 +1,21 @@
import type React from 'react';
import type { VirtuosoHandle } from 'react-virtuoso';
const selectChild = (index: number, handle: React.RefObject<VirtuosoHandle>, node: ParentNode = document) => {
const selector = `[data-index="${index}"] .focusable`;
const element = node.querySelector<HTMLDivElement>(selector);
if (element) {
element.focus({ preventScroll: false });
} else {
handle.current?.scrollIntoView({
index,
behavior: 'smooth',
done: () => {
if (!element) document.querySelector<HTMLDivElement>(selector)?.focus();
},
});
}
};
export { selectChild };