mirror of
https://github.com/cheeaun/phanpy.git
synced 2025-01-23 17:16:26 +01:00
Add j/k keyboard navigation to status page
At the same time, fix shift+k not working in Home page
This commit is contained in:
parent
ded6420c1a
commit
816653e2e6
5 changed files with 192 additions and 97 deletions
|
@ -365,7 +365,7 @@ a[href^='http'][rel*='nofollow']:visited:not(:has(div)) {
|
|||
-webkit-tap-highlight-color: transparent;
|
||||
animation: appear 0.2s ease-out;
|
||||
}
|
||||
.status-link:is(:focus, .is-active) {
|
||||
:is(.status-link, .status-focus):is(:focus, .is-active) {
|
||||
background-color: var(--link-bg-hover-color);
|
||||
outline-offset: -2px;
|
||||
}
|
||||
|
|
|
@ -134,6 +134,8 @@ function App() {
|
|||
}, []);
|
||||
|
||||
let location = useLocation();
|
||||
states.currentLocation = location.pathname;
|
||||
|
||||
const locationDeckMap = {
|
||||
'/': 'home-page',
|
||||
'/notifications': 'notifications-page',
|
||||
|
|
|
@ -17,6 +17,7 @@ const LIMIT = 20;
|
|||
|
||||
function Home({ hidden }) {
|
||||
const snapStates = useSnapshot(states);
|
||||
const isHomeLocation = snapStates.currentLocation === '/';
|
||||
const [uiState, setUIState] = useState('default');
|
||||
const [showMore, setShowMore] = useState(false);
|
||||
|
||||
|
@ -134,103 +135,121 @@ function Home({ hidden }) {
|
|||
|
||||
const scrollableRef = useRef();
|
||||
|
||||
useHotkeys('j, shift+j', (_, handler) => {
|
||||
// focus on next status after active status
|
||||
// Traverses .timeline li .status-link, focus on .status-link
|
||||
const activeStatus = document.activeElement.closest(
|
||||
'.status-link, .status-boost-link',
|
||||
);
|
||||
const activeStatusRect = activeStatus?.getBoundingClientRect();
|
||||
const allStatusLinks = Array.from(
|
||||
scrollableRef.current.querySelectorAll(
|
||||
useHotkeys(
|
||||
'j, shift+j',
|
||||
(_, handler) => {
|
||||
// focus on next status after active status
|
||||
// Traverses .timeline li .status-link, focus on .status-link
|
||||
const activeStatus = document.activeElement.closest(
|
||||
'.status-link, .status-boost-link',
|
||||
),
|
||||
);
|
||||
if (
|
||||
activeStatus &&
|
||||
activeStatusRect.top < scrollableRef.current.clientHeight &&
|
||||
activeStatusRect.bottom > 0
|
||||
) {
|
||||
const activeStatusIndex = allStatusLinks.indexOf(activeStatus);
|
||||
let nextStatus = allStatusLinks[activeStatusIndex + 1];
|
||||
if (handler.shift) {
|
||||
// get next status that's not .status-boost-link
|
||||
nextStatus = allStatusLinks.find(
|
||||
(statusLink, index) =>
|
||||
index > activeStatusIndex &&
|
||||
!statusLink.classList.contains('status-boost-link'),
|
||||
);
|
||||
);
|
||||
const activeStatusRect = activeStatus?.getBoundingClientRect();
|
||||
const allStatusLinks = Array.from(
|
||||
scrollableRef.current.querySelectorAll(
|
||||
'.status-link, .status-boost-link',
|
||||
),
|
||||
);
|
||||
if (
|
||||
activeStatus &&
|
||||
activeStatusRect.top < scrollableRef.current.clientHeight &&
|
||||
activeStatusRect.bottom > 0
|
||||
) {
|
||||
const activeStatusIndex = allStatusLinks.indexOf(activeStatus);
|
||||
let nextStatus = allStatusLinks[activeStatusIndex + 1];
|
||||
if (handler.shift) {
|
||||
// get next status that's not .status-boost-link
|
||||
nextStatus = allStatusLinks.find(
|
||||
(statusLink, index) =>
|
||||
index > activeStatusIndex &&
|
||||
!statusLink.classList.contains('status-boost-link'),
|
||||
);
|
||||
}
|
||||
if (nextStatus) {
|
||||
nextStatus.focus();
|
||||
nextStatus.scrollIntoViewIfNeeded?.();
|
||||
}
|
||||
} else {
|
||||
// If active status is not in viewport, get the topmost status-link in viewport
|
||||
const topmostStatusLink = allStatusLinks.find((statusLink) => {
|
||||
const statusLinkRect = statusLink.getBoundingClientRect();
|
||||
return statusLinkRect.top >= 44 && statusLinkRect.left >= 0; // 44 is the magic number for header height, not real
|
||||
});
|
||||
if (topmostStatusLink) {
|
||||
topmostStatusLink.focus();
|
||||
topmostStatusLink.scrollIntoViewIfNeeded?.();
|
||||
}
|
||||
}
|
||||
if (nextStatus) {
|
||||
nextStatus.focus();
|
||||
nextStatus.scrollIntoViewIfNeeded?.();
|
||||
}
|
||||
} else {
|
||||
// If active status is not in viewport, get the topmost status-link in viewport
|
||||
const topmostStatusLink = allStatusLinks.find((statusLink) => {
|
||||
const statusLinkRect = statusLink.getBoundingClientRect();
|
||||
return statusLinkRect.top >= 44 && statusLinkRect.left >= 0; // 44 is the magic number for header height, not real
|
||||
});
|
||||
if (topmostStatusLink) {
|
||||
topmostStatusLink.focus();
|
||||
topmostStatusLink.scrollIntoViewIfNeeded?.();
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
{
|
||||
enabled: isHomeLocation,
|
||||
},
|
||||
);
|
||||
|
||||
useHotkeys('k, shift+k', (_, handler) => {
|
||||
// focus on previous status after active status
|
||||
// Traverses .timeline li .status-link, focus on .status-link
|
||||
const activeStatus = document.activeElement.closest(
|
||||
'.status-link, .status-boost-link',
|
||||
);
|
||||
const activeStatusRect = activeStatus?.getBoundingClientRect();
|
||||
const allStatusLinks = Array.from(
|
||||
scrollableRef.current.querySelectorAll(
|
||||
useHotkeys(
|
||||
'k, shift+k',
|
||||
(_, handler) => {
|
||||
// focus on previous status after active status
|
||||
// Traverses .timeline li .status-link, focus on .status-link
|
||||
const activeStatus = document.activeElement.closest(
|
||||
'.status-link, .status-boost-link',
|
||||
),
|
||||
);
|
||||
if (
|
||||
activeStatus &&
|
||||
activeStatusRect.top < scrollableRef.current.clientHeight &&
|
||||
activeStatusRect.bottom > 0
|
||||
) {
|
||||
const activeStatusIndex = allStatusLinks.indexOf(activeStatus);
|
||||
let prevStatus = allStatusLinks[activeStatusIndex - 1];
|
||||
if (handler.shift) {
|
||||
// get prev status that's not .status-boost-link
|
||||
prevStatus = allStatusLinks.find(
|
||||
(statusLink, index) =>
|
||||
index < activeStatusIndex &&
|
||||
!statusLink.classList.contains('status-boost-link'),
|
||||
);
|
||||
);
|
||||
const activeStatusRect = activeStatus?.getBoundingClientRect();
|
||||
const allStatusLinks = Array.from(
|
||||
scrollableRef.current.querySelectorAll(
|
||||
'.status-link, .status-boost-link',
|
||||
),
|
||||
);
|
||||
if (
|
||||
activeStatus &&
|
||||
activeStatusRect.top < scrollableRef.current.clientHeight &&
|
||||
activeStatusRect.bottom > 0
|
||||
) {
|
||||
const activeStatusIndex = allStatusLinks.indexOf(activeStatus);
|
||||
let prevStatus = allStatusLinks[activeStatusIndex - 1];
|
||||
if (handler.shift) {
|
||||
// get prev status that's not .status-boost-link
|
||||
prevStatus = allStatusLinks.findLast(
|
||||
(statusLink, index) =>
|
||||
index < activeStatusIndex &&
|
||||
!statusLink.classList.contains('status-boost-link'),
|
||||
);
|
||||
}
|
||||
if (prevStatus) {
|
||||
prevStatus.focus();
|
||||
prevStatus.scrollIntoViewIfNeeded?.();
|
||||
}
|
||||
} else {
|
||||
// If active status is not in viewport, get the topmost status-link in viewport
|
||||
const topmostStatusLink = allStatusLinks.find((statusLink) => {
|
||||
const statusLinkRect = statusLink.getBoundingClientRect();
|
||||
return statusLinkRect.top >= 44 && statusLinkRect.left >= 0; // 44 is the magic number for header height, not real
|
||||
});
|
||||
if (topmostStatusLink) {
|
||||
topmostStatusLink.focus();
|
||||
topmostStatusLink.scrollIntoViewIfNeeded?.();
|
||||
}
|
||||
}
|
||||
if (prevStatus) {
|
||||
prevStatus.focus();
|
||||
prevStatus.scrollIntoViewIfNeeded?.();
|
||||
}
|
||||
} else {
|
||||
// If active status is not in viewport, get the topmost status-link in viewport
|
||||
const topmostStatusLink = allStatusLinks.find((statusLink) => {
|
||||
const statusLinkRect = statusLink.getBoundingClientRect();
|
||||
return statusLinkRect.top >= 44 && statusLinkRect.left >= 0; // 44 is the magic number for header height, not real
|
||||
});
|
||||
if (topmostStatusLink) {
|
||||
topmostStatusLink.focus();
|
||||
topmostStatusLink.scrollIntoViewIfNeeded?.();
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
{
|
||||
enabled: isHomeLocation,
|
||||
},
|
||||
);
|
||||
|
||||
useHotkeys(['enter', 'o'], () => {
|
||||
// open active status
|
||||
const activeStatus = document.activeElement.closest(
|
||||
'.status-link, .status-boost-link',
|
||||
);
|
||||
if (activeStatus) {
|
||||
activeStatus.click();
|
||||
}
|
||||
});
|
||||
useHotkeys(
|
||||
['enter', 'o'],
|
||||
() => {
|
||||
// open active status
|
||||
const activeStatus = document.activeElement.closest(
|
||||
'.status-link, .status-boost-link',
|
||||
);
|
||||
if (activeStatus) {
|
||||
activeStatus.click();
|
||||
}
|
||||
},
|
||||
{
|
||||
enabled: isHomeLocation,
|
||||
},
|
||||
);
|
||||
|
||||
const {
|
||||
scrollDirection,
|
||||
|
|
|
@ -5,7 +5,7 @@ import pRetry from 'p-retry';
|
|||
import { useEffect, useMemo, useRef, useState } from 'preact/hooks';
|
||||
import { useHotkeys } from 'react-hotkeys-hook';
|
||||
import { InView } from 'react-intersection-observer';
|
||||
import { useLocation, useParams } from 'react-router-dom';
|
||||
import { useLocation, useNavigate, useParams } from 'react-router-dom';
|
||||
import { useSnapshot } from 'valtio';
|
||||
|
||||
import Icon from '../components/icon';
|
||||
|
@ -33,6 +33,7 @@ function resetScrollPosition(id) {
|
|||
function StatusPage() {
|
||||
const { id } = useParams();
|
||||
const location = useLocation();
|
||||
const navigate = useNavigate();
|
||||
const snapStates = useSnapshot(states);
|
||||
const [statuses, setStatuses] = useState([]);
|
||||
const [uiState, setUIState] = useState('default');
|
||||
|
@ -317,7 +318,73 @@ function StatusPage() {
|
|||
}, [heroInView]);
|
||||
|
||||
useHotkeys(['esc', 'backspace'], () => {
|
||||
location.hash = closeLink;
|
||||
// location.hash = closeLink;
|
||||
navigate(closeLink);
|
||||
});
|
||||
|
||||
useHotkeys('j', () => {
|
||||
const activeStatus = document.activeElement.closest(
|
||||
'.status-link, .status-focus',
|
||||
);
|
||||
const activeStatusRect = activeStatus?.getBoundingClientRect();
|
||||
const allStatusLinks = Array.from(
|
||||
scrollableRef.current.querySelectorAll('.status-link, .status-focus'),
|
||||
);
|
||||
console.log({ allStatusLinks });
|
||||
if (
|
||||
activeStatus &&
|
||||
activeStatusRect.top < scrollableRef.current.clientHeight &&
|
||||
activeStatusRect.bottom > 0
|
||||
) {
|
||||
const activeStatusIndex = allStatusLinks.indexOf(activeStatus);
|
||||
let nextStatus = allStatusLinks[activeStatusIndex + 1];
|
||||
if (nextStatus) {
|
||||
nextStatus.focus();
|
||||
nextStatus.scrollIntoViewIfNeeded?.();
|
||||
}
|
||||
} else {
|
||||
// If active status is not in viewport, get the topmost status-link in viewport
|
||||
const topmostStatusLink = allStatusLinks.find((statusLink) => {
|
||||
const statusLinkRect = statusLink.getBoundingClientRect();
|
||||
return statusLinkRect.top >= 44 && statusLinkRect.left >= 0; // 44 is the magic number for header height, not real
|
||||
});
|
||||
if (topmostStatusLink) {
|
||||
topmostStatusLink.focus();
|
||||
topmostStatusLink.scrollIntoViewIfNeeded?.();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
useHotkeys('k', () => {
|
||||
const activeStatus = document.activeElement.closest(
|
||||
'.status-link, .status-focus',
|
||||
);
|
||||
const activeStatusRect = activeStatus?.getBoundingClientRect();
|
||||
const allStatusLinks = Array.from(
|
||||
scrollableRef.current.querySelectorAll('.status-link, .status-focus'),
|
||||
);
|
||||
if (
|
||||
activeStatus &&
|
||||
activeStatusRect.top < scrollableRef.current.clientHeight &&
|
||||
activeStatusRect.bottom > 0
|
||||
) {
|
||||
const activeStatusIndex = allStatusLinks.indexOf(activeStatus);
|
||||
let prevStatus = allStatusLinks[activeStatusIndex - 1];
|
||||
if (prevStatus) {
|
||||
prevStatus.focus();
|
||||
prevStatus.scrollIntoViewIfNeeded?.();
|
||||
}
|
||||
} else {
|
||||
// If active status is not in viewport, get the topmost status-link in viewport
|
||||
const topmostStatusLink = allStatusLinks.find((statusLink) => {
|
||||
const statusLinkRect = statusLink.getBoundingClientRect();
|
||||
return statusLinkRect.top >= 44 && statusLinkRect.left >= 0; // 44 is the magic number for header height, not real
|
||||
});
|
||||
if (topmostStatusLink) {
|
||||
topmostStatusLink.focus();
|
||||
topmostStatusLink.scrollIntoViewIfNeeded?.();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const { nearReachStart } = useScroll({
|
||||
|
@ -434,7 +501,12 @@ function StatusPage() {
|
|||
} ${thread ? 'thread' : ''} ${isHero ? 'hero' : ''}`}
|
||||
>
|
||||
{isHero ? (
|
||||
<InView threshold={0.1} onChange={onView}>
|
||||
<InView
|
||||
threshold={0.1}
|
||||
onChange={onView}
|
||||
class="status-focus"
|
||||
tabIndex={0}
|
||||
>
|
||||
<Status statusID={statusID} withinContext size="l" />
|
||||
</InView>
|
||||
) : (
|
||||
|
|
|
@ -3,11 +3,13 @@ import { proxy, subscribe } from 'valtio';
|
|||
import store from './store';
|
||||
|
||||
const states = proxy({
|
||||
history: [],
|
||||
// history: [],
|
||||
prevLocation: null,
|
||||
currentLocation: null,
|
||||
statuses: {},
|
||||
statusThreadNumber: {},
|
||||
home: [],
|
||||
specialHome: [],
|
||||
// specialHome: [],
|
||||
homeNew: [],
|
||||
homeLast: null, // Last item in 'home' list
|
||||
homeLastFetchTime: null,
|
||||
|
|
Loading…
Reference in a new issue