Skip to content

Commit

Permalink
Merge pull request #2583 from opossum-tool/feat-list-keyboard-navigation
Browse files Browse the repository at this point in the history
feat: add keyboard navigation to attributions and signals lists
  • Loading branch information
mstykow authored Mar 6, 2024
2 parents 11e2df2 + 23173fd commit 3748d74
Show file tree
Hide file tree
Showing 21 changed files with 735 additions and 369 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import { changeSelectedAttributionOrOpenUnsavedPopup } from '../../../../state/a
import { useAppDispatch } from '../../../../state/hooks';
import { useAttributionIdsForReplacement } from '../../../../state/variables/use-attribution-ids-for-replacement';
import { isPackageInfoIncomplete } from '../../../../util/is-important-attribution-information-missing';
import { List } from '../../../List/List';
import { List, ListItemContentProps } from '../../../List/List';
import { PackageCard } from '../../../PackageCard/PackageCard';
import { PackagesPanelChildrenProps } from '../../PackagesPanel/PackagesPanel';

Expand All @@ -31,13 +31,16 @@ export const AttributionsList: React.FC<PackagesPanelChildrenProps> = ({
<List
renderItemContent={renderAttributionCard}
data={activeAttributionIds}
selected={selectedAttributionId}
selectedId={selectedAttributionId}
loading={loading}
sx={{ transition: TRANSITION, height: contentHeight }}
/>
);

function renderAttributionCard(attributionId: string) {
function renderAttributionCard(
attributionId: string,
{ selected, focused }: ListItemContentProps,
) {
const attribution = attributions?.[attributionId];

if (!attribution) {
Expand All @@ -54,7 +57,8 @@ export const AttributionsList: React.FC<PackagesPanelChildrenProps> = ({
);
}}
cardConfig={{
selected: attributionId === selectedAttributionId,
selected,
focused,
resolved: attributionIdsForReplacement.includes(attributionId),
incomplete: isPackageInfoIncomplete(attribution),
}}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,10 @@ import {
getResolvedExternalAttributions,
} from '../../../../state/selectors/resource-selectors';
import { useAttributionIdsForReplacement } from '../../../../state/variables/use-attribution-ids-for-replacement';
import { GroupedList } from '../../../GroupedList/GroupedList';
import {
GroupedList,
GroupedListItemContentProps,
} from '../../../GroupedList/GroupedList';
import { SourceIcon } from '../../../Icons/Icons';
import { PackageCard } from '../../../PackageCard/PackageCard';
import { PackagesPanelChildrenProps } from '../../PackagesPanel/PackagesPanel';
Expand Down Expand Up @@ -66,7 +69,7 @@ export const SignalsList: React.FC<PackagesPanelChildrenProps> = ({
return (
<GroupedList
grouped={groupedIds}
selected={selectedAttributionId}
selectedId={selectedAttributionId}
renderItemContent={renderAttributionCard}
renderGroupName={(sourceName) => (
<>
Expand All @@ -79,7 +82,10 @@ export const SignalsList: React.FC<PackagesPanelChildrenProps> = ({
/>
);

function renderAttributionCard(attributionId: string) {
function renderAttributionCard(
attributionId: string,
{ focused, selected }: GroupedListItemContentProps,
) {
const attribution = attributions?.[attributionId];

if (!attribution) {
Expand All @@ -96,7 +102,8 @@ export const SignalsList: React.FC<PackagesPanelChildrenProps> = ({
);
}}
cardConfig={{
selected: attributionId === selectedAttributionId,
selected,
focused,
resolved: resolvedExternalAttributionIds.has(attributionId),
}}
packageInfo={attribution}
Expand Down
5 changes: 3 additions & 2 deletions src/Frontend/Components/CardList/CardList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,14 @@ import { styled } from '@mui/material';

import { OpossumColors } from '../../shared-styles';
import { List } from '../List/List';
import { LIST_CARD_HEIGHT } from '../ListCard/ListCard';
import { PACKAGE_CARD_HEIGHT } from '../PackageCard/PackageCard';

const MAX_NUMBER_OF_CARDS = 4;

export const CardList = styled(List)(({ data }) => {
const height =
Math.min(MAX_NUMBER_OF_CARDS, data?.length ?? 0) * (LIST_CARD_HEIGHT + 1) +
Math.min(MAX_NUMBER_OF_CARDS, data?.length ?? 0) *
(PACKAGE_CARD_HEIGHT + 1) +
1;

return {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,7 @@ export const ConfirmDeletePopup: React.FC<Props> = ({
</MuiTypography>
<CardList
data={attributionIdsToDelete}
renderItemContent={(attributionId, index) => {
renderItemContent={(attributionId, { index }) => {
if (!attributionId || !(attributionId in attributions)) {
return null;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ export const ConfirmReplacePopup = ({
<CardList
data={attributionIdsForReplacement}
data-testid={'removed-attributions'}
renderItemContent={(attributionId, index) => {
renderItemContent={(attributionId, { index }) => {
if (!attributionId || !(attributionId in attributions)) {
return null;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,7 @@ export const ConfirmSavePopup: React.FC<Props> = ({
</MuiTypography>
<CardList
data={attributionIdsToSave}
renderItemContent={(attributionId, index) => {
renderItemContent={(attributionId, { index }) => {
if (!attributionId || !(attributionId in attributions)) {
return null;
}
Expand Down
56 changes: 36 additions & 20 deletions src/Frontend/Components/GroupedList/GroupedList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,7 @@ import { styled } from '@mui/material';
import MuiBox from '@mui/material/Box';
import MuiTooltip from '@mui/material/Tooltip';
import { SxProps } from '@mui/system';
import { defer } from 'lodash';
import { useEffect, useMemo, useRef, useState } from 'react';
import { useMemo, useState } from 'react';
import {
GroupedVirtuoso,
GroupedVirtuosoHandle,
Expand All @@ -20,17 +19,27 @@ import {

import { text } from '../../../shared/text';
import { OpossumColors } from '../../shared-styles';
import { useVirtuosoRefs } from '../../util/use-virtuoso-refs';
import { LoadingMask } from '../LoadingMask/LoadingMask';
import { NoResults } from '../NoResults/NoResults';
import { GroupContainer, StyledLinearProgress } from './GroupedList.style';

interface GroupedListProps {
export interface GroupedListItemContentProps {
index: number;
selected: boolean;
focused: boolean;
}

export interface GroupedListProps {
className?: string;
grouped: Record<string, ReadonlyArray<string>> | null;
loading?: boolean;
renderGroupName?: (key: string) => React.ReactNode;
renderItemContent: (datum: string, index: number) => React.ReactNode;
selected?: string;
renderItemContent: (
datum: string,
props: GroupedListItemContentProps,
) => React.ReactNode;
selectedId?: string;
sx?: SxProps;
testId?: string;
}
Expand All @@ -41,12 +50,11 @@ export function GroupedList({
loading,
renderGroupName,
renderItemContent,
selected,
selectedId,
sx,
testId,
...props
}: GroupedListProps & Omit<GroupedVirtuosoProps<string, unknown>, 'selected'>) {
const ref = useRef<GroupedVirtuosoHandle>(null);
const [{ startIndex, endIndex }, setRange] = useState<{
startIndex: number;
endIndex: number;
Expand All @@ -63,20 +71,19 @@ export function GroupedList({
ids: flattened,
keys: Object.keys(grouped),
counts: Object.values(grouped).map((group) => group.length),
selectedIndex: flattened.findIndex((datum) => datum === selected),
};
}, [grouped, selected]);
}, [grouped]);

useEffect(() => {
if (groups?.selectedIndex !== undefined && groups.selectedIndex >= 0) {
defer(() =>
ref.current?.scrollIntoView({
index: groups.selectedIndex,
align: 'center',
}),
);
}
}, [groups?.selectedIndex]);
const {
ref,
scrollerRef,
focusedIndex,
setIsVirtuosoFocused,
selectedIndex,
} = useVirtuosoRefs<GroupedVirtuosoHandle>({
data: groups?.ids,
selectedId,
});

return (
<LoadingMask
Expand All @@ -89,10 +96,13 @@ export function GroupedList({
{groups && (
<GroupedVirtuoso
ref={ref}
onFocus={() => setIsVirtuosoFocused(true)}
onBlur={() => setIsVirtuosoFocused(false)}
components={{
EmptyPlaceholder:
loading || groups.ids.length ? undefined : () => <NoResults />,
}}
scrollerRef={scrollerRef}
rangeChanged={setRange}
groupCounts={groups?.counts}
groupContent={(index) => (
Expand All @@ -106,7 +116,13 @@ export function GroupedList({
)}
</GroupContainer>
)}
itemContent={(index) => renderItemContent(groups.ids[index], index)}
itemContent={(index) =>
renderItemContent(groups.ids[index], {
index,
selected: index === selectedIndex,
focused: index === focusedIndex,
})
}
{...props}
/>
)}
Expand Down
61 changes: 34 additions & 27 deletions src/Frontend/Components/List/List.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,20 +3,28 @@
//
// SPDX-License-Identifier: Apache-2.0
import { SxProps } from '@mui/system';
import { defer } from 'lodash';
import { useEffect, useMemo, useRef } from 'react';
import { Virtuoso, VirtuosoHandle, VirtuosoProps } from 'react-virtuoso';

import { useVirtuosoRefs } from '../../util/use-virtuoso-refs';
import { LoadingMask } from '../LoadingMask/LoadingMask';
import { NoResults } from '../NoResults/NoResults';
import { StyledLinearProgress } from './List.style';

interface ListProps {
export interface ListItemContentProps {
index: number;
selected: boolean;
focused: boolean;
}

export interface ListProps {
className?: string;
data: ReadonlyArray<string> | null;
loading?: boolean;
renderItemContent: (datum: string, index: number) => React.ReactNode;
selected?: string;
renderItemContent: (
datum: string,
props: ListItemContentProps,
) => React.ReactNode;
selectedId?: string;
sx?: SxProps;
testId?: string;
}
Expand All @@ -26,31 +34,21 @@ export function List({
data,
loading,
renderItemContent,
selected,
selectedId,
sx,
testId,
...props
}: ListProps & Omit<VirtuosoProps<string, unknown>, 'data' | 'selected'>) {
const ref = useRef<VirtuosoHandle>(null);

const selectedIndex = useMemo(() => {
if (!data) {
return undefined;
}

return data.findIndex((datum) => datum === selected);
}, [data, selected]);

useEffect(() => {
if (selectedIndex !== undefined && selectedIndex >= 0) {
defer(() =>
ref.current?.scrollIntoView({
index: selectedIndex,
align: 'center',
}),
);
}
}, [selectedIndex]);
const {
focusedIndex,
ref,
scrollerRef,
setIsVirtuosoFocused,
selectedIndex,
} = useVirtuosoRefs<VirtuosoHandle>({
data,
selectedId,
});

return (
<LoadingMask
Expand All @@ -63,12 +61,21 @@ export function List({
{data && (
<Virtuoso
ref={ref}
onFocus={() => setIsVirtuosoFocused(true)}
onBlur={() => setIsVirtuosoFocused(false)}
components={{
EmptyPlaceholder:
loading || data.length ? undefined : () => <NoResults />,
}}
scrollerRef={scrollerRef}
data={data}
itemContent={(index) => renderItemContent(data[index], index)}
itemContent={(index) =>
renderItemContent(data[index], {
index,
selected: index === selectedIndex,
focused: index === focusedIndex,
})
}
{...props}
/>
)}
Expand Down
Loading

0 comments on commit 3748d74

Please sign in to comment.