mirror of
https://github.com/cheeaun/phanpy.git
synced 2025-02-02 22:26:57 +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;
|
-webkit-tap-highlight-color: transparent;
|
||||||
animation: appear 0.2s ease-out;
|
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);
|
background-color: var(--link-bg-hover-color);
|
||||||
outline-offset: -2px;
|
outline-offset: -2px;
|
||||||
}
|
}
|
||||||
|
|
|
@ -134,6 +134,8 @@ function App() {
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
let location = useLocation();
|
let location = useLocation();
|
||||||
|
states.currentLocation = location.pathname;
|
||||||
|
|
||||||
const locationDeckMap = {
|
const locationDeckMap = {
|
||||||
'/': 'home-page',
|
'/': 'home-page',
|
||||||
'/notifications': 'notifications-page',
|
'/notifications': 'notifications-page',
|
||||||
|
|
|
@ -17,6 +17,7 @@ const LIMIT = 20;
|
||||||
|
|
||||||
function Home({ hidden }) {
|
function Home({ hidden }) {
|
||||||
const snapStates = useSnapshot(states);
|
const snapStates = useSnapshot(states);
|
||||||
|
const isHomeLocation = snapStates.currentLocation === '/';
|
||||||
const [uiState, setUIState] = useState('default');
|
const [uiState, setUIState] = useState('default');
|
||||||
const [showMore, setShowMore] = useState(false);
|
const [showMore, setShowMore] = useState(false);
|
||||||
|
|
||||||
|
@ -134,103 +135,121 @@ function Home({ hidden }) {
|
||||||
|
|
||||||
const scrollableRef = useRef();
|
const scrollableRef = useRef();
|
||||||
|
|
||||||
useHotkeys('j, shift+j', (_, handler) => {
|
useHotkeys(
|
||||||
// focus on next status after active status
|
'j, shift+j',
|
||||||
// Traverses .timeline li .status-link, focus on .status-link
|
(_, handler) => {
|
||||||
const activeStatus = document.activeElement.closest(
|
// focus on next status after active status
|
||||||
'.status-link, .status-boost-link',
|
// Traverses .timeline li .status-link, focus on .status-link
|
||||||
);
|
const activeStatus = document.activeElement.closest(
|
||||||
const activeStatusRect = activeStatus?.getBoundingClientRect();
|
|
||||||
const allStatusLinks = Array.from(
|
|
||||||
scrollableRef.current.querySelectorAll(
|
|
||||||
'.status-link, .status-boost-link',
|
'.status-link, .status-boost-link',
|
||||||
),
|
);
|
||||||
);
|
const activeStatusRect = activeStatus?.getBoundingClientRect();
|
||||||
if (
|
const allStatusLinks = Array.from(
|
||||||
activeStatus &&
|
scrollableRef.current.querySelectorAll(
|
||||||
activeStatusRect.top < scrollableRef.current.clientHeight &&
|
'.status-link, .status-boost-link',
|
||||||
activeStatusRect.bottom > 0
|
),
|
||||||
) {
|
);
|
||||||
const activeStatusIndex = allStatusLinks.indexOf(activeStatus);
|
if (
|
||||||
let nextStatus = allStatusLinks[activeStatusIndex + 1];
|
activeStatus &&
|
||||||
if (handler.shift) {
|
activeStatusRect.top < scrollableRef.current.clientHeight &&
|
||||||
// get next status that's not .status-boost-link
|
activeStatusRect.bottom > 0
|
||||||
nextStatus = allStatusLinks.find(
|
) {
|
||||||
(statusLink, index) =>
|
const activeStatusIndex = allStatusLinks.indexOf(activeStatus);
|
||||||
index > activeStatusIndex &&
|
let nextStatus = allStatusLinks[activeStatusIndex + 1];
|
||||||
!statusLink.classList.contains('status-boost-link'),
|
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?.();
|
enabled: isHomeLocation,
|
||||||
}
|
},
|
||||||
} 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, shift+k', (_, handler) => {
|
useHotkeys(
|
||||||
// focus on previous status after active status
|
'k, shift+k',
|
||||||
// Traverses .timeline li .status-link, focus on .status-link
|
(_, handler) => {
|
||||||
const activeStatus = document.activeElement.closest(
|
// focus on previous status after active status
|
||||||
'.status-link, .status-boost-link',
|
// Traverses .timeline li .status-link, focus on .status-link
|
||||||
);
|
const activeStatus = document.activeElement.closest(
|
||||||
const activeStatusRect = activeStatus?.getBoundingClientRect();
|
|
||||||
const allStatusLinks = Array.from(
|
|
||||||
scrollableRef.current.querySelectorAll(
|
|
||||||
'.status-link, .status-boost-link',
|
'.status-link, .status-boost-link',
|
||||||
),
|
);
|
||||||
);
|
const activeStatusRect = activeStatus?.getBoundingClientRect();
|
||||||
if (
|
const allStatusLinks = Array.from(
|
||||||
activeStatus &&
|
scrollableRef.current.querySelectorAll(
|
||||||
activeStatusRect.top < scrollableRef.current.clientHeight &&
|
'.status-link, .status-boost-link',
|
||||||
activeStatusRect.bottom > 0
|
),
|
||||||
) {
|
);
|
||||||
const activeStatusIndex = allStatusLinks.indexOf(activeStatus);
|
if (
|
||||||
let prevStatus = allStatusLinks[activeStatusIndex - 1];
|
activeStatus &&
|
||||||
if (handler.shift) {
|
activeStatusRect.top < scrollableRef.current.clientHeight &&
|
||||||
// get prev status that's not .status-boost-link
|
activeStatusRect.bottom > 0
|
||||||
prevStatus = allStatusLinks.find(
|
) {
|
||||||
(statusLink, index) =>
|
const activeStatusIndex = allStatusLinks.indexOf(activeStatus);
|
||||||
index < activeStatusIndex &&
|
let prevStatus = allStatusLinks[activeStatusIndex - 1];
|
||||||
!statusLink.classList.contains('status-boost-link'),
|
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?.();
|
enabled: isHomeLocation,
|
||||||
}
|
},
|
||||||
} 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(['enter', 'o'], () => {
|
useHotkeys(
|
||||||
// open active status
|
['enter', 'o'],
|
||||||
const activeStatus = document.activeElement.closest(
|
() => {
|
||||||
'.status-link, .status-boost-link',
|
// open active status
|
||||||
);
|
const activeStatus = document.activeElement.closest(
|
||||||
if (activeStatus) {
|
'.status-link, .status-boost-link',
|
||||||
activeStatus.click();
|
);
|
||||||
}
|
if (activeStatus) {
|
||||||
});
|
activeStatus.click();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
enabled: isHomeLocation,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
scrollDirection,
|
scrollDirection,
|
||||||
|
|
|
@ -5,7 +5,7 @@ import pRetry from 'p-retry';
|
||||||
import { useEffect, useMemo, useRef, useState } from 'preact/hooks';
|
import { useEffect, useMemo, useRef, useState } from 'preact/hooks';
|
||||||
import { useHotkeys } from 'react-hotkeys-hook';
|
import { useHotkeys } from 'react-hotkeys-hook';
|
||||||
import { InView } from 'react-intersection-observer';
|
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 { useSnapshot } from 'valtio';
|
||||||
|
|
||||||
import Icon from '../components/icon';
|
import Icon from '../components/icon';
|
||||||
|
@ -33,6 +33,7 @@ function resetScrollPosition(id) {
|
||||||
function StatusPage() {
|
function StatusPage() {
|
||||||
const { id } = useParams();
|
const { id } = useParams();
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
|
const navigate = useNavigate();
|
||||||
const snapStates = useSnapshot(states);
|
const snapStates = useSnapshot(states);
|
||||||
const [statuses, setStatuses] = useState([]);
|
const [statuses, setStatuses] = useState([]);
|
||||||
const [uiState, setUIState] = useState('default');
|
const [uiState, setUIState] = useState('default');
|
||||||
|
@ -317,7 +318,73 @@ function StatusPage() {
|
||||||
}, [heroInView]);
|
}, [heroInView]);
|
||||||
|
|
||||||
useHotkeys(['esc', 'backspace'], () => {
|
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({
|
const { nearReachStart } = useScroll({
|
||||||
|
@ -434,7 +501,12 @@ function StatusPage() {
|
||||||
} ${thread ? 'thread' : ''} ${isHero ? 'hero' : ''}`}
|
} ${thread ? 'thread' : ''} ${isHero ? 'hero' : ''}`}
|
||||||
>
|
>
|
||||||
{isHero ? (
|
{isHero ? (
|
||||||
<InView threshold={0.1} onChange={onView}>
|
<InView
|
||||||
|
threshold={0.1}
|
||||||
|
onChange={onView}
|
||||||
|
class="status-focus"
|
||||||
|
tabIndex={0}
|
||||||
|
>
|
||||||
<Status statusID={statusID} withinContext size="l" />
|
<Status statusID={statusID} withinContext size="l" />
|
||||||
</InView>
|
</InView>
|
||||||
) : (
|
) : (
|
||||||
|
|
|
@ -3,11 +3,13 @@ import { proxy, subscribe } from 'valtio';
|
||||||
import store from './store';
|
import store from './store';
|
||||||
|
|
||||||
const states = proxy({
|
const states = proxy({
|
||||||
history: [],
|
// history: [],
|
||||||
|
prevLocation: null,
|
||||||
|
currentLocation: null,
|
||||||
statuses: {},
|
statuses: {},
|
||||||
statusThreadNumber: {},
|
statusThreadNumber: {},
|
||||||
home: [],
|
home: [],
|
||||||
specialHome: [],
|
// specialHome: [],
|
||||||
homeNew: [],
|
homeNew: [],
|
||||||
homeLast: null, // Last item in 'home' list
|
homeLast: null, // Last item in 'home' list
|
||||||
homeLastFetchTime: null,
|
homeLastFetchTime: null,
|
||||||
|
|
Loading…
Reference in a new issue