Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit 45258c4

Browse files
committedFeb 27, 2024··
feat: add keyboard navigation to attributions and signals lists
- merge PackageCard and ListCard components - track keyboard selected item in list and grouped-list - add focus styling to package card closes #971 Signed-off-by: Maxim Stykow <maxim.stykow@tngtech.com>
1 parent fbf99e6 commit 45258c4

File tree

19 files changed

+554
-329
lines changed

19 files changed

+554
-329
lines changed
 

‎src/Frontend/Components/AttributionPanels/AttributionsPanel/AttributionsList/AttributionsList.tsx

+7-3
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import { changeSelectedAttributionOrOpenUnsavedPopup } from '../../../../state/a
1010
import { useAppDispatch } from '../../../../state/hooks';
1111
import { useAttributionIdsForReplacement } from '../../../../state/variables/use-attribution-ids-for-replacement';
1212
import { isPackageInfoIncomplete } from '../../../../util/is-important-attribution-information-missing';
13-
import { List } from '../../../List/List';
13+
import { List, ListItemContentProps } from '../../../List/List';
1414
import { PackageCard } from '../../../PackageCard/PackageCard';
1515
import { PackagesPanelChildrenProps } from '../../PackagesPanel/PackagesPanel';
1616

@@ -37,7 +37,10 @@ export const AttributionsList: React.FC<PackagesPanelChildrenProps> = ({
3737
/>
3838
);
3939

40-
function renderAttributionCard(attributionId: string) {
40+
function renderAttributionCard(
41+
attributionId: string,
42+
{ selected, focused }: ListItemContentProps,
43+
) {
4144
const attribution = attributions?.[attributionId];
4245

4346
if (!attribution) {
@@ -54,7 +57,8 @@ export const AttributionsList: React.FC<PackagesPanelChildrenProps> = ({
5457
);
5558
}}
5659
cardConfig={{
57-
selected: attributionId === selectedAttributionId,
60+
selected,
61+
focused,
5862
resolved: attributionIdsForReplacement.includes(attributionId),
5963
incomplete: isPackageInfoIncomplete(attribution),
6064
}}

‎src/Frontend/Components/AttributionPanels/SignalsPanel/SignalsList/SignalsList.tsx

+10-3
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,10 @@ import {
1414
getResolvedExternalAttributions,
1515
} from '../../../../state/selectors/resource-selectors';
1616
import { useAttributionIdsForReplacement } from '../../../../state/variables/use-attribution-ids-for-replacement';
17-
import { GroupedList } from '../../../GroupedList/GroupedList';
17+
import {
18+
GroupedList,
19+
GroupedListItemContentProps,
20+
} from '../../../GroupedList/GroupedList';
1821
import { SourceIcon } from '../../../Icons/Icons';
1922
import { PackageCard } from '../../../PackageCard/PackageCard';
2023
import { PackagesPanelChildrenProps } from '../../PackagesPanel/PackagesPanel';
@@ -79,7 +82,10 @@ export const SignalsList: React.FC<PackagesPanelChildrenProps> = ({
7982
/>
8083
);
8184

82-
function renderAttributionCard(attributionId: string) {
85+
function renderAttributionCard(
86+
attributionId: string,
87+
{ focused, selected }: GroupedListItemContentProps,
88+
) {
8389
const attribution = attributions?.[attributionId];
8490

8591
if (!attribution) {
@@ -96,7 +102,8 @@ export const SignalsList: React.FC<PackagesPanelChildrenProps> = ({
96102
);
97103
}}
98104
cardConfig={{
99-
selected: attributionId === selectedAttributionId,
105+
selected,
106+
focused,
100107
resolved: resolvedExternalAttributionIds.has(attributionId),
101108
}}
102109
packageInfo={attribution}

‎src/Frontend/Components/CardList/CardList.tsx

+3-2
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,14 @@ import { styled } from '@mui/material';
66

77
import { OpossumColors } from '../../shared-styles';
88
import { List } from '../List/List';
9-
import { LIST_CARD_HEIGHT } from '../ListCard/ListCard';
9+
import { PACKAGE_CARD_HEIGHT } from '../PackageCard/PackageCard';
1010

1111
const MAX_NUMBER_OF_CARDS = 4;
1212

1313
export const CardList = styled(List)(({ data }) => {
1414
const height =
15-
Math.min(MAX_NUMBER_OF_CARDS, data?.length ?? 0) * (LIST_CARD_HEIGHT + 1) +
15+
Math.min(MAX_NUMBER_OF_CARDS, data?.length ?? 0) *
16+
(PACKAGE_CARD_HEIGHT + 1) +
1617
1;
1718

1819
return {

‎src/Frontend/Components/ConfirmDeletionPopup/ConfirmDeletionPopup.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -124,7 +124,7 @@ export const ConfirmDeletionPopup: React.FC<Props> = ({
124124
</MuiTypography>
125125
<CardList
126126
data={attributionIdsToDelete}
127-
renderItemContent={(attributionId, index) => {
127+
renderItemContent={(attributionId, { index }) => {
128128
if (!attributionId || !(attributionId in attributions)) {
129129
return null;
130130
}

‎src/Frontend/Components/ConfirmSavePopup/ConfirmSavePopup.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -153,7 +153,7 @@ export const ConfirmSavePopup: React.FC<Props> = ({
153153
</MuiTypography>
154154
<CardList
155155
data={attributionIdsToSave}
156-
renderItemContent={(attributionId, index) => {
156+
renderItemContent={(attributionId, { index }) => {
157157
if (!attributionId || !(attributionId in attributions)) {
158158
return null;
159159
}

‎src/Frontend/Components/GroupedList/GroupedList.tsx

+27-5
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import MuiBox from '@mui/material/Box';
1010
import MuiTooltip from '@mui/material/Tooltip';
1111
import { SxProps } from '@mui/system';
1212
import { defer } from 'lodash';
13-
import { useEffect, useMemo, useRef, useState } from 'react';
13+
import { useEffect, useMemo, useState } from 'react';
1414
import {
1515
GroupedVirtuoso,
1616
GroupedVirtuosoHandle,
@@ -19,16 +19,26 @@ import {
1919

2020
import { text } from '../../../shared/text';
2121
import { OpossumColors } from '../../shared-styles';
22+
import { useVirtuosoRefs } from '../../util/use-virtuoso-refs';
2223
import { LoadingMask } from '../LoadingMask/LoadingMask';
2324
import { NoResults } from '../NoResults/NoResults';
2425
import { GroupContainer, StyledLinearProgress } from './GroupedList.style';
2526

27+
export interface GroupedListItemContentProps {
28+
index: number;
29+
selected: boolean;
30+
focused: boolean;
31+
}
32+
2633
export interface GroupedListProps {
2734
className?: string;
2835
grouped: Record<string, ReadonlyArray<string>> | null;
2936
loading?: boolean;
3037
renderGroupName?: (key: string) => React.ReactNode;
31-
renderItemContent: (datum: string, index: number) => React.ReactNode;
38+
renderItemContent: (
39+
datum: string,
40+
props: GroupedListItemContentProps,
41+
) => React.ReactNode;
3242
selected?: string;
3343
sx?: SxProps;
3444
testId?: string;
@@ -45,7 +55,6 @@ export function GroupedList({
4555
testId,
4656
...props
4757
}: GroupedListProps & Omit<GroupedVirtuosoProps<string, unknown>, 'selected'>) {
48-
const ref = useRef<GroupedVirtuosoHandle>(null);
4958
const [{ startIndex, endIndex }, setRange] = useState<{
5059
startIndex: number;
5160
endIndex: number;
@@ -66,6 +75,12 @@ export function GroupedList({
6675
};
6776
}, [grouped, selected]);
6877

78+
const { ref, scrollerRef, focusedIndex } =
79+
useVirtuosoRefs<GroupedVirtuosoHandle>({
80+
data: groups?.ids,
81+
selectedIndex: groups?.selectedIndex,
82+
});
83+
6984
useEffect(() => {
7085
if (groups?.selectedIndex !== undefined && groups.selectedIndex >= 0) {
7186
defer(() =>
@@ -75,7 +90,7 @@ export function GroupedList({
7590
}),
7691
);
7792
}
78-
}, [groups?.selectedIndex]);
93+
}, [groups?.selectedIndex, ref]);
7994

8095
return (
8196
<LoadingMask
@@ -101,6 +116,7 @@ export function GroupedList({
101116
EmptyPlaceholder:
102117
loading || groups.ids.length ? undefined : () => <NoResults />,
103118
}}
119+
scrollerRef={scrollerRef}
104120
rangeChanged={setRange}
105121
groupCounts={groups?.counts}
106122
groupContent={(index) => (
@@ -114,7 +130,13 @@ export function GroupedList({
114130
)}
115131
</GroupContainer>
116132
)}
117-
itemContent={(index) => renderItemContent(groups.ids[index], index)}
133+
itemContent={(index) =>
134+
renderItemContent(groups.ids[index], {
135+
index,
136+
selected: index === groups.selectedIndex,
137+
focused: index === focusedIndex,
138+
})
139+
}
118140
{...props}
119141
/>
120142
);

‎src/Frontend/Components/List/List.tsx

+26-6
Original file line numberDiff line numberDiff line change
@@ -4,18 +4,28 @@
44
// SPDX-License-Identifier: Apache-2.0
55
import { SxProps } from '@mui/system';
66
import { defer } from 'lodash';
7-
import { useEffect, useMemo, useRef } from 'react';
7+
import { useEffect, useMemo } from 'react';
88
import { Virtuoso, VirtuosoHandle, VirtuosoProps } from 'react-virtuoso';
99

10+
import { useVirtuosoRefs } from '../../util/use-virtuoso-refs';
1011
import { LoadingMask } from '../LoadingMask/LoadingMask';
1112
import { NoResults } from '../NoResults/NoResults';
1213
import { StyledLinearProgress } from './List.style';
1314

15+
export interface ListItemContentProps {
16+
index: number;
17+
selected: boolean;
18+
focused: boolean;
19+
}
20+
1421
export interface ListProps {
1522
className?: string;
1623
data: ReadonlyArray<string> | null;
1724
loading?: boolean;
18-
renderItemContent: (datum: string, index: number) => React.ReactNode;
25+
renderItemContent: (
26+
datum: string,
27+
props: ListItemContentProps,
28+
) => React.ReactNode;
1929
selected?: string;
2030
sx?: SxProps;
2131
testId?: string;
@@ -31,8 +41,6 @@ export function List({
3141
testId,
3242
...props
3343
}: ListProps & Omit<VirtuosoProps<string, unknown>, 'data' | 'selected'>) {
34-
const ref = useRef<VirtuosoHandle>(null);
35-
3644
const selectedIndex = useMemo(() => {
3745
if (!data) {
3846
return undefined;
@@ -41,6 +49,11 @@ export function List({
4149
return data.findIndex((datum) => datum === selected);
4250
}, [data, selected]);
4351

52+
const { focusedIndex, ref, scrollerRef } = useVirtuosoRefs<VirtuosoHandle>({
53+
data,
54+
selectedIndex,
55+
});
56+
4457
useEffect(() => {
4558
if (selectedIndex !== undefined && selectedIndex >= 0) {
4659
defer(() =>
@@ -50,7 +63,7 @@ export function List({
5063
}),
5164
);
5265
}
53-
}, [selectedIndex]);
66+
}, [selectedIndex, ref]);
5467

5568
return (
5669
<LoadingMask
@@ -76,8 +89,15 @@ export function List({
7689
EmptyPlaceholder:
7790
loading || data.length ? undefined : () => <NoResults />,
7891
}}
92+
scrollerRef={scrollerRef}
7993
data={data}
80-
itemContent={(index) => renderItemContent(data[index], index)}
94+
itemContent={(index) =>
95+
renderItemContent(data[index], {
96+
index,
97+
selected: index === selectedIndex,
98+
focused: index === focusedIndex,
99+
})
100+
}
81101
{...props}
82102
/>
83103
);

0 commit comments

Comments
 (0)
Please sign in to comment.