From 081d37a45977af7ffb88884c550feae287510b31 Mon Sep 17 00:00:00 2001 From: schroda <50052685+schroda@users.noreply.github.com> Date: Thu, 15 Jan 2026 00:53:49 +0100 Subject: [PATCH] Make source language filter language header sticky --- .../components/SourceLanguageSelect.tsx | 137 +++++++++++------- 1 file changed, 83 insertions(+), 54 deletions(-) diff --git a/src/features/source/components/SourceLanguageSelect.tsx b/src/features/source/components/SourceLanguageSelect.tsx index 29e8942d..ef740641 100644 --- a/src/features/source/components/SourceLanguageSelect.tsx +++ b/src/features/source/components/SourceLanguageSelect.tsx @@ -6,7 +6,7 @@ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ -import { useMemo, useState } from 'react'; +import { useCallback, useMemo, useState } from 'react'; import Button from '@mui/material/Button'; import DialogTitle from '@mui/material/DialogTitle'; import DialogContent from '@mui/material/DialogContent'; @@ -17,8 +17,7 @@ import IconButton from '@mui/material/IconButton'; import FilterListIcon from '@mui/icons-material/FilterList'; import ListItemText from '@mui/material/ListItemText'; import ListItem from '@mui/material/ListItem'; -import { Virtuoso } from 'react-virtuoso'; -import Box from '@mui/material/Box'; +import { GroupedVirtuoso } from 'react-virtuoso'; import ListItemAvatar from '@mui/material/ListItemAvatar'; import { useLingui } from '@lingui/react/macro'; import Checkbox from '@mui/material/Checkbox'; @@ -39,6 +38,7 @@ import { requestManager } from '@/lib/requests/RequestManager.ts'; import { ListCardAvatar } from '@/base/components/lists/cards/ListCardAvatar.tsx'; import { makeToast } from '@/base/utils/Toast.ts'; import { getErrorMessage } from '@/lib/HelperFunctions.ts'; +import { VirtuosoUtil } from '@/lib/virtuoso/Virtuoso.util.tsx'; export const SourceLanguageSelect = ({ selectedLanguages, @@ -74,6 +74,33 @@ export const SourceLanguageSelect = ({ [languages, tmpSelectedLanguages], ); + const flattenedSourcesByLanguages = useMemo( + () => Object.values(languagesSortedBySelectState.map((language) => sourcesByLanguage[language])).flat(), + [languagesSortedBySelectState, sourcesByLanguage], + ); + + const groupCounts = useMemo( + () => + languagesSortedBySelectState.map((language) => { + const isEnabled = tmpSelectedLanguages.includes(language); + if (!isEnabled) { + return 0; + } + + return sourcesByLanguage[language].length ?? 0; + }), + [sourcesByLanguage, languagesSortedBySelectState], + ); + + const computeItemKey = VirtuosoUtil.useCreateGroupedComputeItemKey( + groupCounts, + useCallback((index) => languagesSortedBySelectState[index], [languagesSortedBySelectState]), + useCallback( + (index) => flattenedSourcesByLanguages[index].id, + [sourcesByLanguage, languagesSortedBySelectState], + ), + ); + const handleCancel = () => { setOpen(false); @@ -111,67 +138,69 @@ export const SourceLanguageSelect = ({ {t`Allowed Languages`} - languagesSortedBySelectState[index]} - itemContent={(_index, language) => { - const sourcesOfLanguage = sourcesByLanguage[language] ?? []; + computeItemKey={computeItemKey} + groupContent={(index) => { + const language = languagesSortedBySelectState[index]; const isEnabled = tmpSelectedLanguages.includes(language); return ( - <> - - - handleChange(language, e.target.checked)} + + + handleChange(language, e.target.checked)} + /> + + ); + }} + itemContent={(index) => { + const source = flattenedSourcesByLanguages[index]; + + return ( + + + - - {isEnabled && !!sourcesOfLanguage.length && ( - - {sourcesOfLanguage.map((source) => ( - - - - - - - setTmpSourceIdToEnabledState({ - ...tmpSourceIdToEnabledState, - [source.id]: e.target.checked, - }) - } - /> - - ))} - - )} - + + + + setTmpSourceIdToEnabledState({ + ...tmpSourceIdToEnabledState, + [source.id]: e.target.checked, + }) + } + /> + ); }} />