mirror of
https://github.com/glitch-soc/mastodon.git
synced 2025-12-14 08:19:05 +00:00
[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:
@@ -112,6 +112,7 @@ const hotkeyMatcherMap = {
|
|||||||
openProfile: just('p'),
|
openProfile: just('p'),
|
||||||
moveDown: just('j'),
|
moveDown: just('j'),
|
||||||
moveUp: just('k'),
|
moveUp: just('k'),
|
||||||
|
moveToTop: just('0'),
|
||||||
toggleHidden: just('x'),
|
toggleHidden: just('x'),
|
||||||
toggleSensitive: just('h'),
|
toggleSensitive: just('h'),
|
||||||
toggleComposeSpoilers: optionPlus('x'),
|
toggleComposeSpoilers: optionPlus('x'),
|
||||||
|
|||||||
@@ -99,13 +99,17 @@ class KeyboardShortcuts extends ImmutablePureComponent {
|
|||||||
<td><FormattedMessage id='keyboard_shortcuts.down' defaultMessage='to move down in the list' /></td>
|
<td><FormattedMessage id='keyboard_shortcuts.down' defaultMessage='to move down in the list' /></td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td><kbd>l</kbd></td>
|
<td><kbd>0</kbd></td>
|
||||||
<td><FormattedMessage id='keyboard_shortcuts.load_more' defaultMessage='Focus "Load more" button' /></td>
|
<td><FormattedMessage id='keyboard_shortcuts.top' defaultMessage='Move to top of list' /></td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td><kbd>1</kbd>-<kbd>9</kbd></td>
|
<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>
|
<td><FormattedMessage id='keyboard_shortcuts.column' defaultMessage='to focus a status in one of the columns' /></td>
|
||||||
</tr>
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><kbd>l</kbd></td>
|
||||||
|
<td><FormattedMessage id='keyboard_shortcuts.load_more' defaultMessage='Focus "Load more" button' /></td>
|
||||||
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td><kbd>n</kbd></td>
|
<td><kbd>n</kbd></td>
|
||||||
<td><FormattedMessage id='keyboard_shortcuts.compose' defaultMessage='to focus the compose textarea' /></td>
|
<td><FormattedMessage id='keyboard_shortcuts.compose' defaultMessage='to focus the compose textarea' /></td>
|
||||||
|
|||||||
@@ -81,7 +81,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 } from './util/focusUtils';
|
import { focusColumn, getFocusedItemIndex, focusItemSibling, focusFirstItem } from './util/focusUtils';
|
||||||
import { WrappedSwitch, WrappedRoute } from './util/react_router_helpers';
|
import { WrappedSwitch, WrappedRoute } from './util/react_router_helpers';
|
||||||
|
|
||||||
// Dummy import, to make sure that <Status /> ends up in the application bundle.
|
// Dummy import, to make sure that <Status /> ends up in the application bundle.
|
||||||
@@ -495,20 +495,21 @@ class UI extends PureComponent {
|
|||||||
};
|
};
|
||||||
|
|
||||||
handleHotkeyFocusColumn = e => {
|
handleHotkeyFocusColumn = e => {
|
||||||
focusColumn({index: e.key * 1});
|
focusColumn(e.key * 1);
|
||||||
};
|
};
|
||||||
|
|
||||||
handleHotkeyLoadMore = () => {
|
handleHotkeyLoadMore = () => {
|
||||||
document.querySelector('.load-more')?.focus();
|
document.querySelector('.load-more')?.focus();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
handleMoveToTop = () => {
|
||||||
|
focusFirstItem();
|
||||||
|
};
|
||||||
|
|
||||||
handleMoveUp = () => {
|
handleMoveUp = () => {
|
||||||
const currentItemIndex = getFocusedItemIndex();
|
const currentItemIndex = getFocusedItemIndex();
|
||||||
if (currentItemIndex === -1) {
|
if (currentItemIndex === -1) {
|
||||||
focusColumn({
|
focusColumn(1);
|
||||||
index: 1,
|
|
||||||
focusItem: 'first-visible',
|
|
||||||
});
|
|
||||||
} else {
|
} else {
|
||||||
focusItemSibling(currentItemIndex, -1);
|
focusItemSibling(currentItemIndex, -1);
|
||||||
}
|
}
|
||||||
@@ -517,10 +518,7 @@ class UI extends PureComponent {
|
|||||||
handleMoveDown = () => {
|
handleMoveDown = () => {
|
||||||
const currentItemIndex = getFocusedItemIndex();
|
const currentItemIndex = getFocusedItemIndex();
|
||||||
if (currentItemIndex === -1) {
|
if (currentItemIndex === -1) {
|
||||||
focusColumn({
|
focusColumn(1);
|
||||||
index: 1,
|
|
||||||
focusItem: 'first-visible',
|
|
||||||
});
|
|
||||||
} else {
|
} else {
|
||||||
focusItemSibling(currentItemIndex, 1);
|
focusItemSibling(currentItemIndex, 1);
|
||||||
}
|
}
|
||||||
@@ -615,6 +613,7 @@ class UI extends PureComponent {
|
|||||||
focusLoadMore: this.handleHotkeyLoadMore,
|
focusLoadMore: this.handleHotkeyLoadMore,
|
||||||
moveDown: this.handleMoveDown,
|
moveDown: this.handleMoveDown,
|
||||||
moveUp: this.handleMoveUp,
|
moveUp: this.handleMoveUp,
|
||||||
|
moveToTop: this.handleMoveToTop,
|
||||||
back: this.handleHotkeyBack,
|
back: this.handleHotkeyBack,
|
||||||
goToHome: this.handleHotkeyGoToHome,
|
goToHome: this.handleHotkeyGoToHome,
|
||||||
goToNotifications: this.handleHotkeyGoToNotifications,
|
goToNotifications: this.handleHotkeyGoToNotifications,
|
||||||
|
|||||||
@@ -1,16 +1,30 @@
|
|||||||
interface FocusColumnOptions {
|
/**
|
||||||
index?: number;
|
* Out of a list of elements, return the first one whose top edge
|
||||||
focusItem?: 'first' | 'first-visible';
|
* 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).
|
* 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({
|
export function focusColumn(index = 1) {
|
||||||
index = 1,
|
|
||||||
focusItem = 'first',
|
|
||||||
}: FocusColumnOptions = {}) {
|
|
||||||
// Skip the leftmost drawer in multi-column mode
|
// Skip the leftmost drawer in multi-column mode
|
||||||
const isMultiColumnLayout = !!document.querySelector(
|
const isMultiColumnLayout = !!document.querySelector(
|
||||||
'body.layout-multiple-columns',
|
'body.layout-multiple-columns',
|
||||||
@@ -27,33 +41,28 @@ export function focusColumn({
|
|||||||
|
|
||||||
if (!container) return;
|
if (!container) return;
|
||||||
|
|
||||||
let itemToFocus: HTMLElement | null = null;
|
const focusableItems = Array.from(
|
||||||
|
container.querySelectorAll<HTMLElement>(
|
||||||
|
'.focusable:not(.status__quote .focusable)',
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
if (focusItem === 'first-visible') {
|
// Find first item visible in the viewport
|
||||||
const focusableItems = Array.from(
|
const itemToFocus = findFirstVisibleWithRect(focusableItems);
|
||||||
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');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (itemToFocus) {
|
if (itemToFocus) {
|
||||||
if (container.scrollTop > itemToFocus.offsetTop) {
|
const viewportWidth =
|
||||||
itemToFocus.scrollIntoView(true);
|
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);
|
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
|
* Focus the item next to the one with the provided index
|
||||||
*/
|
*/
|
||||||
|
|||||||
Reference in New Issue
Block a user