[Glitch] Don't reset scroll when using hotkeys to focus columns, add hotkey 0 to scroll to top

Port 9334bd9ede to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
This commit is contained in:
diondiondion
2025-11-28 17:19:23 +01:00
committed by Claire
parent 34aa825e96
commit c63393c963
4 changed files with 76 additions and 43 deletions

View File

@@ -112,6 +112,7 @@ const hotkeyMatcherMap = {
openProfile: just('p'),
moveDown: just('j'),
moveUp: just('k'),
moveToTop: just('0'),
toggleHidden: just('x'),
toggleSensitive: just('h'),
toggleComposeSpoilers: optionPlus('x'),

View File

@@ -99,13 +99,17 @@ class KeyboardShortcuts extends ImmutablePureComponent {
<td><FormattedMessage id='keyboard_shortcuts.down' defaultMessage='to move down in the list' /></td>
</tr>
<tr>
<td><kbd>l</kbd></td>
<td><FormattedMessage id='keyboard_shortcuts.load_more' defaultMessage='Focus "Load more" button' /></td>
<td><kbd>0</kbd></td>
<td><FormattedMessage id='keyboard_shortcuts.top' defaultMessage='Move to top of list' /></td>
</tr>
<tr>
<td><kbd>1</kbd>-<kbd>9</kbd></td>
<td><FormattedMessage id='keyboard_shortcuts.column' defaultMessage='to focus a status in one of the columns' /></td>
</tr>
<tr>
<td><kbd>l</kbd></td>
<td><FormattedMessage id='keyboard_shortcuts.load_more' defaultMessage='Focus "Load more" button' /></td>
</tr>
<tr>
<td><kbd>n</kbd></td>
<td><FormattedMessage id='keyboard_shortcuts.compose' defaultMessage='to focus the compose textarea' /></td>

View File

@@ -81,7 +81,7 @@ import {
Quotes,
} from './util/async-components';
import { ColumnsContextProvider } from './util/columns_context';
import { focusColumn, getFocusedItemIndex, focusItemSibling } from './util/focusUtils';
import { focusColumn, getFocusedItemIndex, focusItemSibling, focusFirstItem } from './util/focusUtils';
import { WrappedSwitch, WrappedRoute } from './util/react_router_helpers';
// Dummy import, to make sure that <Status /> ends up in the application bundle.
@@ -495,20 +495,21 @@ class UI extends PureComponent {
};
handleHotkeyFocusColumn = e => {
focusColumn({index: e.key * 1});
focusColumn(e.key * 1);
};
handleHotkeyLoadMore = () => {
document.querySelector('.load-more')?.focus();
};
handleMoveToTop = () => {
focusFirstItem();
};
handleMoveUp = () => {
const currentItemIndex = getFocusedItemIndex();
if (currentItemIndex === -1) {
focusColumn({
index: 1,
focusItem: 'first-visible',
});
focusColumn(1);
} else {
focusItemSibling(currentItemIndex, -1);
}
@@ -517,10 +518,7 @@ class UI extends PureComponent {
handleMoveDown = () => {
const currentItemIndex = getFocusedItemIndex();
if (currentItemIndex === -1) {
focusColumn({
index: 1,
focusItem: 'first-visible',
});
focusColumn(1);
} else {
focusItemSibling(currentItemIndex, 1);
}
@@ -615,6 +613,7 @@ class UI extends PureComponent {
focusLoadMore: this.handleHotkeyLoadMore,
moveDown: this.handleMoveDown,
moveUp: this.handleMoveUp,
moveToTop: this.handleMoveToTop,
back: this.handleHotkeyBack,
goToHome: this.handleHotkeyGoToHome,
goToNotifications: this.handleHotkeyGoToNotifications,

View File

@@ -1,16 +1,30 @@
interface FocusColumnOptions {
index?: number;
focusItem?: 'first' | 'first-visible';
/**
* Out of a list of elements, return the first one whose top edge
* is inside of the viewport, and return the element and its BoundingClientRect.
*/
function findFirstVisibleWithRect(
items: HTMLElement[],
): { item: HTMLElement; rect: DOMRect } | null {
const viewportHeight =
window.innerHeight || document.documentElement.clientHeight;
for (const item of items) {
const rect = item.getBoundingClientRect();
const isVisible = rect.top >= 0 && rect.top < viewportHeight;
if (isVisible) {
return { item, rect };
}
}
return null;
}
/**
* Move focus to the column of the passed index (1-based).
* Can either focus the topmost item or the first one in the viewport
* Focus is placed on the topmost visible item
*/
export function focusColumn({
index = 1,
focusItem = 'first',
}: FocusColumnOptions = {}) {
export function focusColumn(index = 1) {
// Skip the leftmost drawer in multi-column mode
const isMultiColumnLayout = !!document.querySelector(
'body.layout-multiple-columns',
@@ -27,33 +41,28 @@ export function focusColumn({
if (!container) return;
let itemToFocus: HTMLElement | null = null;
const focusableItems = Array.from(
container.querySelectorAll<HTMLElement>(
'.focusable:not(.status__quote .focusable)',
),
);
if (focusItem === 'first-visible') {
const focusableItems = Array.from(
container.querySelectorAll<HTMLElement>(
'.focusable:not(.status__quote .focusable)',
),
);
const viewportHeight =
window.innerHeight || document.documentElement.clientHeight;
// Find first item visible in the viewport
itemToFocus =
focusableItems.find((item) => {
const { top } = item.getBoundingClientRect();
return top >= 0 && top < viewportHeight;
}) ?? null;
} else {
itemToFocus = container.querySelector('.focusable');
}
// Find first item visible in the viewport
const itemToFocus = findFirstVisibleWithRect(focusableItems);
if (itemToFocus) {
if (container.scrollTop > itemToFocus.offsetTop) {
itemToFocus.scrollIntoView(true);
const viewportWidth =
window.innerWidth || document.documentElement.clientWidth;
const { item, rect } = itemToFocus;
if (
container.scrollTop > item.offsetTop ||
rect.right > viewportWidth ||
rect.left < 0
) {
itemToFocus.item.scrollIntoView(true);
}
itemToFocus.focus();
itemToFocus.item.focus();
}
}
@@ -71,6 +80,26 @@ export function getFocusedItemIndex() {
return items.indexOf(focusedItem);
}
/**
* Focus the topmost item of the column that currently has focus,
* or the first column if none
*/
export function focusFirstItem() {
const focusedElement = document.activeElement;
const container =
focusedElement?.closest('.scrollable') ??
document.querySelector('.scrollable');
if (!container) return;
const itemToFocus = container.querySelector<HTMLElement>('.focusable');
if (itemToFocus) {
container.scrollTo(0, 0);
itemToFocus.focus();
}
}
/**
* Focus the item next to the one with the provided index
*/