From 9aebdbe4c3eaac3cf2db1a2b7e15b07d881ee1ad Mon Sep 17 00:00:00 2001 From: Hugo Gomes Date: Fri, 30 Oct 2026 18:47:12 +0000 Subject: [PATCH 1/3] fix(drawer): only restore a selector grid's state while its item count is unchanged --- src/features/drawer/components/SelectorList.jsx | 7 ++++--- src/features/drawer/hooks/useScrollMemory.js | 15 ++++++++++----- 2 files changed, 14 insertions(+), 8 deletions(-) diff --git a/src/features/drawer/components/SelectorList.jsx b/src/features/drawer/components/SelectorList.jsx index ae9e02cc7..d8f964da1 100644 --- a/src/features/drawer/components/SelectorList.jsx +++ b/src/features/drawer/components/SelectorList.jsx @@ -111,8 +111,9 @@ function SelectorList({ .map((item) => item.id) }, [translated, search]) + // read once per mount, the grid only applies it when it mounts const restoreStateFrom = React.useMemo( - () => getDrawerGridState(listScrollKey), + () => getDrawerGridState(listScrollKey, items.length), [listScrollKey], ) const shouldPersistGridState = drawer && visible @@ -132,9 +133,9 @@ function SelectorList({ ) { return } - setDrawerGridState(listScrollKey, state) + setDrawerGridState(listScrollKey, state, items.length) }, - [listScrollKey, shouldPersistGridState], + [listScrollKey, shouldPersistGridState, items.length], ) /** @param {'enable' | 'disable' | 'advanced'} action */ diff --git a/src/features/drawer/hooks/useScrollMemory.js b/src/features/drawer/hooks/useScrollMemory.js index d0892a576..58247b7be 100644 --- a/src/features/drawer/hooks/useScrollMemory.js +++ b/src/features/drawer/hooks/useScrollMemory.js @@ -1,26 +1,31 @@ // @ts-check import * as React from 'react' -/** @type {Map} */ +/** @type {Map} */ const gridStateMemory = new Map() /** @type {Map} */ const scrollTopMemory = new Map() /** + * The grid's saved state, only while the list still has as many items as when it was saved: a snapshot of a + * list that has since grown or shrunk (e.g. tappable rewards coming and going) restores to an empty window * @param {string} key + * @param {number} count * @returns {import('react-virtuoso').GridStateSnapshot | null} */ -export function getDrawerGridState(key) { - return gridStateMemory.get(key) || null +export function getDrawerGridState(key, count) { + const saved = gridStateMemory.get(key) + return saved && saved.count === count ? saved.state : null } /** * @param {string} key * @param {import('react-virtuoso').GridStateSnapshot} state + * @param {number} count */ -export function setDrawerGridState(key, state) { - gridStateMemory.set(key, state) +export function setDrawerGridState(key, state, count) { + gridStateMemory.set(key, { state, count }) } /** From 8f5cebe5fc7430fac4efd94dc3c9625cf5239a20 Mon Sep 17 00:00:00 2001 From: Hugo Gomes Date: Fri, 30 Oct 2026 19:04:56 +0000 Subject: [PATCH 2/3] fix(drawer): also drop the saved scroll offset when a grid's item count changed --- src/features/drawer/hooks/useScrollMemory.js | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/features/drawer/hooks/useScrollMemory.js b/src/features/drawer/hooks/useScrollMemory.js index 58247b7be..9261dcbb6 100644 --- a/src/features/drawer/hooks/useScrollMemory.js +++ b/src/features/drawer/hooks/useScrollMemory.js @@ -9,14 +9,21 @@ const scrollTopMemory = new Map() /** * The grid's saved state, only while the list still has as many items as when it was saved: a snapshot of a - * list that has since grown or shrunk (e.g. tappable rewards coming and going) restores to an empty window + * list that has since grown or shrunk (e.g. tappable rewards coming and going) restores to an empty window. + * Dropping it also drops the saved scroll offset, which `useDrawerScrollMemory` would otherwise still restore, + * so a changed list starts at the top. Call it before the scroller mounts * @param {string} key * @param {number} count * @returns {import('react-virtuoso').GridStateSnapshot | null} */ export function getDrawerGridState(key, count) { const saved = gridStateMemory.get(key) - return saved && saved.count === count ? saved.state : null + if (saved && saved.count === count) return saved.state + if (saved) { + gridStateMemory.delete(key) + scrollTopMemory.delete(key) + } + return null } /** From 342ad904cf42ddc9a3f1b526048267a50f6598f5 Mon Sep 17 00:00:00 2001 From: Hugo Gomes Date: Fri, 30 Oct 2026 19:14:43 +0000 Subject: [PATCH 3/3] fix(drawer): restore only the scroll offset of selector grids, not a grid state snapshot --- .../drawer/components/SelectorList.jsx | 43 +++---------------- src/features/drawer/hooks/useScrollMemory.js | 31 ------------- 2 files changed, 5 insertions(+), 69 deletions(-) diff --git a/src/features/drawer/components/SelectorList.jsx b/src/features/drawer/components/SelectorList.jsx index d8f964da1..c80c13b0f 100644 --- a/src/features/drawer/components/SelectorList.jsx +++ b/src/features/drawer/components/SelectorList.jsx @@ -29,11 +29,7 @@ import { BoolToggle } from '@components/inputs/BoolToggle' import { GenericSearchMemo } from '@components/inputs/GenericSearch' import { StandardItem } from '@components/virtual/StandardItem' -import { - getDrawerGridState, - setDrawerGridState, - useDrawerScrollMemory, -} from '../hooks/useScrollMemory' +import { useDrawerScrollMemory } from '../hooks/useScrollMemory' /** * @template {keyof import('@rm/types').Available} T @@ -111,32 +107,9 @@ function SelectorList({ .map((item) => item.id) }, [translated, search]) - // read once per mount, the grid only applies it when it mounts - const restoreStateFrom = React.useMemo( - () => getDrawerGridState(listScrollKey, items.length), - [listScrollKey], - ) - const shouldPersistGridState = drawer && visible - const scrollMemory = useDrawerScrollMemory( - listScrollKey, - shouldPersistGridState, - ) - - const handleStateChanged = React.useCallback( - (state) => { - if ( - !shouldPersistGridState || - !state.viewport.height || - !state.viewport.width || - !state.item.height || - !state.item.width - ) { - return - } - setDrawerGridState(listScrollKey, state, items.length) - }, - [listScrollKey, shouldPersistGridState, items.length], - ) + // only the scroll offset is restored: the grid works out which tiles to draw from it. Also restoring a + // grid state snapshot made the two disagree, and the grid drew a range that wasn't on screen (blank tiles) + const scrollMemory = useDrawerScrollMemory(listScrollKey, drawer && visible) /** @param {'enable' | 'disable' | 'advanced'} action */ const setAll = (action) => { @@ -227,13 +200,7 @@ function SelectorList({ : height } > - + {(_, key) => ( } */ -const gridStateMemory = new Map() - /** @type {Map} */ const scrollTopMemory = new Map() -/** - * The grid's saved state, only while the list still has as many items as when it was saved: a snapshot of a - * list that has since grown or shrunk (e.g. tappable rewards coming and going) restores to an empty window. - * Dropping it also drops the saved scroll offset, which `useDrawerScrollMemory` would otherwise still restore, - * so a changed list starts at the top. Call it before the scroller mounts - * @param {string} key - * @param {number} count - * @returns {import('react-virtuoso').GridStateSnapshot | null} - */ -export function getDrawerGridState(key, count) { - const saved = gridStateMemory.get(key) - if (saved && saved.count === count) return saved.state - if (saved) { - gridStateMemory.delete(key) - scrollTopMemory.delete(key) - } - return null -} - -/** - * @param {string} key - * @param {import('react-virtuoso').GridStateSnapshot} state - * @param {number} count - */ -export function setDrawerGridState(key, state, count) { - gridStateMemory.set(key, { state, count }) -} - /** * @template {HTMLElement} T * @param {string} key