[Glitch] Make post/feed navigation by hotkey more robust

Port 09689103390d10d77de5146483cbbb8abb5c94c9 to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
This commit is contained in:
diondiondion 2026-06-05 10:39:23 +02:00 committed by Claire
parent a147932ce6
commit f0c48790a8
2 changed files with 69 additions and 19 deletions

View File

@ -89,7 +89,7 @@ import {
Quotes, Quotes,
} from './util/async-components'; } from './util/async-components';
import { ColumnsContextProvider } from './util/columns_context'; import { ColumnsContextProvider } from './util/columns_context';
import { focusColumn, getFocusedItemIndex, focusItemSibling, focusFirstItem } from './util/focusUtils'; import { focusColumn, getFocusedItemIndex, focusItemSibling, focusFirstItem, getFocusedColumnIndex } from './util/focusUtils';
import { WrappedSwitch, WrappedRoute } from './util/react_router_helpers'; import { WrappedSwitch, WrappedRoute } from './util/react_router_helpers';
import { CustomHomepage } from 'flavours/glitch/features/custom_homepage'; import { CustomHomepage } from 'flavours/glitch/features/custom_homepage';
@ -552,20 +552,18 @@ class UI extends PureComponent {
handleMoveUp = () => { handleMoveUp = () => {
const currentItemIndex = getFocusedItemIndex(); const currentItemIndex = getFocusedItemIndex();
if (currentItemIndex === -1) { if (currentItemIndex === -1) {
focusColumn(1); return focusColumn(getFocusedColumnIndex());
} else { } else {
const wasHandled = focusItemSibling(currentItemIndex, -1); return focusItemSibling(currentItemIndex, -1);
return wasHandled;
} }
}; };
handleMoveDown = () => { handleMoveDown = () => {
const currentItemIndex = getFocusedItemIndex(); const currentItemIndex = getFocusedItemIndex();
if (currentItemIndex === -1) { if (currentItemIndex === -1) {
focusColumn(1); return focusColumn(getFocusedColumnIndex());
} else { } else {
const wasHandled = focusItemSibling(currentItemIndex, 1); return focusItemSibling(currentItemIndex, 1);
return wasHandled;
} }
}; };

View File

@ -62,18 +62,17 @@ export function focusColumn(index = 1) {
function fallback() { function fallback() {
focusColumnTitle(index + indexOffset, isMultiColumnLayout); focusColumnTitle(index + indexOffset, isMultiColumnLayout);
return false;
} }
if (!column) { if (!column) {
fallback(); return fallback();
return;
} }
const container = column.querySelector('.scrollable'); const container = column.querySelector('.scrollable');
if (!container) { if (!container) {
fallback(); return fallback();
return;
} }
const focusableItems = Array.from( const focusableItems = Array.from(
@ -86,8 +85,7 @@ export function focusColumn(index = 1) {
const itemToFocus = findFirstVisibleWithRect(focusableItems); const itemToFocus = findFirstVisibleWithRect(focusableItems);
if (!itemToFocus) { if (!itemToFocus) {
fallback(); return fallback();
return;
} }
const viewportWidth = const viewportWidth =
@ -110,6 +108,7 @@ export function focusColumn(index = 1) {
itemToFocus.item.scrollIntoView(true); itemToFocus.item.scrollIntoView(true);
} }
itemToFocus.item.focus(); itemToFocus.item.focus();
return true;
} }
/** /**
@ -126,6 +125,18 @@ export function getFocusedItemIndex() {
return items.indexOf(focusedItem); return items.indexOf(focusedItem);
} }
/**
* Get the index of the column that contains the user's focus
*/
export function getFocusedColumnIndex() {
const columnWithFocus = document.activeElement?.closest('.column');
if (!columnWithFocus) return 1;
const allColumns = Array.from(document.querySelectorAll('.column'));
return allColumns.indexOf(columnWithFocus) + 1;
}
/** /**
* Focus the topmost item of the column that currently has focus, * Focus the topmost item of the column that currently has focus,
* or the first column if none * or the first column if none
@ -159,12 +170,7 @@ export function focusItemSibling(index: number, direction: 1 | -1) {
); );
if (!siblingItem) { if (!siblingItem) {
return false; return focusListSibling(direction);
}
// If sibling element is empty, we skip it
if (siblingItem.matches(':empty')) {
return focusItemSibling(index + direction, direction);
} }
// Check if the sibling is a post or a 'follow suggestions' widget // Check if the sibling is a post or a 'follow suggestions' widget
@ -177,6 +183,52 @@ export function focusItemSibling(index: number, direction: 1 | -1) {
targetElement = siblingItem; targetElement = siblingItem;
} }
// If sibling element is empty, we skip it
if (!targetElement || siblingItem.matches(':empty')) {
return focusItemSibling(index + direction, direction);
}
targetElement.scrollIntoView({
block: 'start',
});
targetElement.focus();
return true;
}
/**
* Finds the next or previous .item-list on the page or in the column,
* and focuses its first or last item.
*/
function focusListSibling(direction: 1 | -1) {
const container = document.activeElement?.closest('.item-list');
if (!container) {
return false;
}
// Get all item lists in the current column or page
const currentColumn = container.closest('.column') ?? document;
const columnItemLists = Array.from(
currentColumn.querySelectorAll('.item-list'),
);
const currentListIndex = columnItemLists.indexOf(container);
// Find the next or previous item-list
const listSibling = columnItemLists[currentListIndex + direction];
// Depending on the direction, find the first or last focusable in the list
let targetElement: HTMLElement | null | undefined;
if (direction > 0) {
targetElement = listSibling?.querySelector<HTMLElement>('.focusable');
} else {
const allFocusables =
listSibling?.querySelectorAll<HTMLElement>('.focusable');
targetElement = allFocusables?.[allFocusables.length - 1];
}
if (targetElement) { if (targetElement) {
targetElement.scrollIntoView({ targetElement.scrollIntoView({
block: 'start', block: 'start',