diff --git a/app/javascript/flavours/glitch/components/hotkeys/index.tsx b/app/javascript/flavours/glitch/components/hotkeys/index.tsx
index db24038ff8..29d43266e1 100644
--- a/app/javascript/flavours/glitch/components/hotkeys/index.tsx
+++ b/app/javascript/flavours/glitch/components/hotkeys/index.tsx
@@ -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'),
diff --git a/app/javascript/flavours/glitch/features/keyboard_shortcuts/index.jsx b/app/javascript/flavours/glitch/features/keyboard_shortcuts/index.jsx
index e424568586..5a4589aa17 100644
--- a/app/javascript/flavours/glitch/features/keyboard_shortcuts/index.jsx
+++ b/app/javascript/flavours/glitch/features/keyboard_shortcuts/index.jsx
@@ -99,13 +99,17 @@ class KeyboardShortcuts extends ImmutablePureComponent {
|
- | l |
- |
+ 0 |
+ |
| 1-9 |
|
+
+ | l |
+ |
+
| n |
|
diff --git a/app/javascript/flavours/glitch/features/ui/index.jsx b/app/javascript/flavours/glitch/features/ui/index.jsx
index f18c44c4c8..9cf4b2372e 100644
--- a/app/javascript/flavours/glitch/features/ui/index.jsx
+++ b/app/javascript/flavours/glitch/features/ui/index.jsx
@@ -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 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,
diff --git a/app/javascript/flavours/glitch/features/ui/util/focusUtils.ts b/app/javascript/flavours/glitch/features/ui/util/focusUtils.ts
index a728a3c5eb..f140cb0b35 100644
--- a/app/javascript/flavours/glitch/features/ui/util/focusUtils.ts
+++ b/app/javascript/flavours/glitch/features/ui/util/focusUtils.ts
@@ -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(
+ '.focusable:not(.status__quote .focusable)',
+ ),
+ );
- if (focusItem === 'first-visible') {
- const focusableItems = Array.from(
- container.querySelectorAll(
- '.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('.focusable');
+
+ if (itemToFocus) {
+ container.scrollTo(0, 0);
+ itemToFocus.focus();
+ }
+}
+
/**
* Focus the item next to the one with the provided index
*/