Skip to content
This repository was archived by the owner on Sep 11, 2024. It is now read-only.

Commit 54a66bd

Browse files
author
Kerry
authored
Device manager - scroll to filtered list from security recommendations (PSG-640) (#9227)
* scroll to filtered list from security recommendations * test sessionmanager scroll to * stable snapshot * fix strict errors * prtidy * use smooth scrolling
1 parent 0d6a550 commit 54a66bd

File tree

8 files changed

+204
-94
lines changed

8 files changed

+204
-94
lines changed

src/components/views/settings/devices/FilteredDeviceList.tsx

Lines changed: 66 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
1414
limitations under the License.
1515
*/
1616

17-
import React from 'react';
17+
import React, { ForwardedRef, forwardRef } from 'react';
1818

1919
import { _t } from '../../../../languageHandler';
2020
import AccessibleButton from '../../elements/AccessibleButton';
@@ -150,70 +150,69 @@ const DeviceListItem: React.FC<{
150150
* Filtered list of devices
151151
* Sorted by latest activity descending
152152
*/
153-
const FilteredDeviceList: React.FC<Props> = ({
154-
devices,
155-
filter,
156-
expandedDeviceIds,
157-
onFilterChange,
158-
onDeviceExpandToggle,
159-
}) => {
160-
const sortedDevices = getFilteredSortedDevices(devices, filter);
161-
162-
const options: FilterDropdownOption<DeviceFilterKey>[] = [
163-
{ id: ALL_FILTER_ID, label: _t('All') },
164-
{
165-
id: DeviceSecurityVariation.Verified,
166-
label: _t('Verified'),
167-
description: _t('Ready for secure messaging'),
168-
},
169-
{
170-
id: DeviceSecurityVariation.Unverified,
171-
label: _t('Unverified'),
172-
description: _t('Not ready for secure messaging'),
173-
},
174-
{
175-
id: DeviceSecurityVariation.Inactive,
176-
label: _t('Inactive'),
177-
description: _t(
178-
'Inactive for %(inactiveAgeDays)s days or longer',
179-
{ inactiveAgeDays: INACTIVE_DEVICE_AGE_DAYS },
180-
),
181-
},
182-
];
183-
184-
const onFilterOptionChange = (filterId: DeviceFilterKey) => {
185-
onFilterChange(filterId === ALL_FILTER_ID ? undefined : filterId as DeviceSecurityVariation);
186-
};
187-
188-
return <div className='mx_FilteredDeviceList'>
189-
<div className='mx_FilteredDeviceList_header'>
190-
<span className='mx_FilteredDeviceList_headerLabel'>
191-
{ _t('Sessions') }
192-
</span>
193-
<FilterDropdown<DeviceFilterKey>
194-
id='device-list-filter'
195-
label={_t('Filter devices')}
196-
value={filter || ALL_FILTER_ID}
197-
onOptionChange={onFilterOptionChange}
198-
options={options}
199-
selectedLabel={_t('Show')}
200-
/>
201-
</div>
202-
{ !!sortedDevices.length
203-
? <FilterSecurityCard filter={filter} />
204-
: <NoResults filter={filter} clearFilter={() => onFilterChange(undefined)} />
205-
}
206-
<ol className='mx_FilteredDeviceList_list'>
207-
{ sortedDevices.map((device) => <DeviceListItem
208-
key={device.device_id}
209-
device={device}
210-
isExpanded={expandedDeviceIds.includes(device.device_id)}
211-
onDeviceExpandToggle={() => onDeviceExpandToggle(device.device_id)}
212-
/>,
213-
) }
214-
</ol>
215-
</div>
216-
;
217-
};
153+
export const FilteredDeviceList =
154+
forwardRef(({
155+
devices,
156+
filter,
157+
expandedDeviceIds,
158+
onFilterChange,
159+
onDeviceExpandToggle,
160+
}: Props, ref: ForwardedRef<HTMLDivElement>) => {
161+
const sortedDevices = getFilteredSortedDevices(devices, filter);
162+
163+
const options: FilterDropdownOption<DeviceFilterKey>[] = [
164+
{ id: ALL_FILTER_ID, label: _t('All') },
165+
{
166+
id: DeviceSecurityVariation.Verified,
167+
label: _t('Verified'),
168+
description: _t('Ready for secure messaging'),
169+
},
170+
{
171+
id: DeviceSecurityVariation.Unverified,
172+
label: _t('Unverified'),
173+
description: _t('Not ready for secure messaging'),
174+
},
175+
{
176+
id: DeviceSecurityVariation.Inactive,
177+
label: _t('Inactive'),
178+
description: _t(
179+
'Inactive for %(inactiveAgeDays)s days or longer',
180+
{ inactiveAgeDays: INACTIVE_DEVICE_AGE_DAYS },
181+
),
182+
},
183+
];
184+
185+
const onFilterOptionChange = (filterId: DeviceFilterKey) => {
186+
onFilterChange(filterId === ALL_FILTER_ID ? undefined : filterId as DeviceSecurityVariation);
187+
};
188+
189+
return <div className='mx_FilteredDeviceList' ref={ref}>
190+
<div className='mx_FilteredDeviceList_header'>
191+
<span className='mx_FilteredDeviceList_headerLabel'>
192+
{ _t('Sessions') }
193+
</span>
194+
<FilterDropdown<DeviceFilterKey>
195+
id='device-list-filter'
196+
label={_t('Filter devices')}
197+
value={filter || ALL_FILTER_ID}
198+
onOptionChange={onFilterOptionChange}
199+
options={options}
200+
selectedLabel={_t('Show')}
201+
/>
202+
</div>
203+
{ !!sortedDevices.length
204+
? <FilterSecurityCard filter={filter} />
205+
: <NoResults filter={filter} clearFilter={() => onFilterChange(undefined)} />
206+
}
207+
<ol className='mx_FilteredDeviceList_list'>
208+
{ sortedDevices.map((device) => <DeviceListItem
209+
key={device.device_id}
210+
device={device}
211+
isExpanded={expandedDeviceIds.includes(device.device_id)}
212+
onDeviceExpandToggle={() => onDeviceExpandToggle(device.device_id)}
213+
/>,
214+
) }
215+
</ol>
216+
</div>;
217+
});
218218

219-
export default FilteredDeviceList;

src/components/views/settings/devices/SecurityRecommendations.tsx

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -29,9 +29,13 @@ import {
2929

3030
interface Props {
3131
devices: DevicesDictionary;
32+
goToFilteredList: (filter: DeviceSecurityVariation) => void;
3233
}
3334

34-
const SecurityRecommendations: React.FC<Props> = ({ devices }) => {
35+
const SecurityRecommendations: React.FC<Props> = ({
36+
devices,
37+
goToFilteredList,
38+
}) => {
3539
const devicesArray = Object.values<DeviceWithVerification>(devices);
3640

3741
const unverifiedDevicesCount = filterDevicesBySecurityRecommendation(
@@ -49,9 +53,6 @@ const SecurityRecommendations: React.FC<Props> = ({ devices }) => {
4953

5054
const inactiveAgeDays = INACTIVE_DEVICE_AGE_DAYS;
5155

52-
// TODO(kerrya) stubbed until PSG-640/652
53-
const noop = () => {};
54-
5556
return <SettingsSubsection
5657
heading={_t('Security recommendations')}
5758
description={_t('Improve your account security by following these recommendations')}
@@ -69,7 +70,8 @@ const SecurityRecommendations: React.FC<Props> = ({ devices }) => {
6970
>
7071
<AccessibleButton
7172
kind='link_inline'
72-
onClick={noop}
73+
onClick={() => goToFilteredList(DeviceSecurityVariation.Unverified)}
74+
data-testid='unverified-devices-cta'
7375
>
7476
{ _t('View all') + ` (${unverifiedDevicesCount})` }
7577
</AccessibleButton>
@@ -90,7 +92,8 @@ const SecurityRecommendations: React.FC<Props> = ({ devices }) => {
9092
>
9193
<AccessibleButton
9294
kind='link_inline'
93-
onClick={noop}
95+
onClick={() => goToFilteredList(DeviceSecurityVariation.Inactive)}
96+
data-testid='inactive-devices-cta'
9497
>
9598
{ _t('View all') + ` (${inactiveDevicesCount})` }
9699
</AccessibleButton>

src/components/views/settings/tabs/user/SessionManagerTab.tsx

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,12 @@ See the License for the specific language governing permissions and
1414
limitations under the License.
1515
*/
1616

17-
import React, { useState } from 'react';
17+
import React, { useEffect, useRef, useState } from 'react';
1818

1919
import { _t } from "../../../../../languageHandler";
2020
import { useOwnDevices } from '../../devices/useOwnDevices';
2121
import SettingsSubsection from '../../shared/SettingsSubsection';
22-
import FilteredDeviceList from '../../devices/FilteredDeviceList';
22+
import { FilteredDeviceList } from '../../devices/FilteredDeviceList';
2323
import CurrentDeviceSection from '../../devices/CurrentDeviceSection';
2424
import SecurityRecommendations from '../../devices/SecurityRecommendations';
2525
import { DeviceSecurityVariation, DeviceWithVerification } from '../../devices/types';
@@ -28,7 +28,9 @@ import SettingsTab from '../SettingsTab';
2828
const SessionManagerTab: React.FC = () => {
2929
const { devices, currentDeviceId, isLoading } = useOwnDevices();
3030
const [filter, setFilter] = useState<DeviceSecurityVariation>();
31-
const [expandedDeviceIds, setExpandedDeviceIds] = useState([]);
31+
const [expandedDeviceIds, setExpandedDeviceIds] = useState<DeviceWithVerification['device_id'][]>([]);
32+
const filteredDeviceListRef = useRef<HTMLDivElement>(null);
33+
const scrollIntoViewTimeoutRef = useRef<ReturnType<typeof setTimeout>>();
3234

3335
const onDeviceExpandToggle = (deviceId: DeviceWithVerification['device_id']): void => {
3436
if (expandedDeviceIds.includes(deviceId)) {
@@ -38,11 +40,29 @@ const SessionManagerTab: React.FC = () => {
3840
}
3941
};
4042

43+
const onGoToFilteredList = (filter: DeviceSecurityVariation) => {
44+
setFilter(filter);
45+
// @TODO(kerrya) clear selection when added in PSG-659
46+
clearTimeout(scrollIntoViewTimeoutRef.current);
47+
// wait a tick for the filtered section to rerender with different height
48+
scrollIntoViewTimeoutRef.current =
49+
window.setTimeout(() => filteredDeviceListRef.current?.scrollIntoView({
50+
// align element to top of scrollbox
51+
block: 'start',
52+
inline: 'nearest',
53+
behavior: 'smooth',
54+
}));
55+
};
56+
4157
const { [currentDeviceId]: currentDevice, ...otherDevices } = devices;
4258
const shouldShowOtherSessions = Object.keys(otherDevices).length > 0;
4359

60+
useEffect(() => () => {
61+
clearTimeout(scrollIntoViewTimeoutRef.current);
62+
}, [scrollIntoViewTimeoutRef]);
63+
4464
return <SettingsTab heading={_t('Sessions')}>
45-
<SecurityRecommendations devices={devices} />
65+
<SecurityRecommendations devices={devices} goToFilteredList={onGoToFilteredList} />
4666
<CurrentDeviceSection
4767
device={currentDevice}
4868
isLoading={isLoading}
@@ -63,6 +83,7 @@ const SessionManagerTab: React.FC = () => {
6383
expandedDeviceIds={expandedDeviceIds}
6484
onFilterChange={setFilter}
6585
onDeviceExpandToggle={onDeviceExpandToggle}
86+
ref={filteredDeviceListRef}
6687
/>
6788
</SettingsSubsection>
6889
}

test/components/views/settings/devices/FilteredDeviceList-test.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ limitations under the License.
1717
import React from 'react';
1818
import { act, fireEvent, render } from '@testing-library/react';
1919

20-
import FilteredDeviceList from '../../../../../src/components/views/settings/devices/FilteredDeviceList';
20+
import { FilteredDeviceList } from '../../../../../src/components/views/settings/devices/FilteredDeviceList';
2121
import { DeviceSecurityVariation } from '../../../../../src/components/views/settings/devices/types';
2222
import { flushPromises, mockPlatformPeg } from '../../../../test-utils';
2323

test/components/views/settings/devices/SecurityRecommendations-test.tsx

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,10 @@ limitations under the License.
1515
*/
1616

1717
import React from 'react';
18-
import { render } from '@testing-library/react';
18+
import { act, fireEvent, render } from '@testing-library/react';
1919

2020
import SecurityRecommendations from '../../../../../src/components/views/settings/devices/SecurityRecommendations';
21+
import { DeviceSecurityVariation } from '../../../../../src/components/views/settings/devices/types';
2122

2223
const MS_DAY = 24 * 60 * 60 * 1000;
2324
describe('<SecurityRecommendations />', () => {
@@ -32,6 +33,7 @@ describe('<SecurityRecommendations />', () => {
3233

3334
const defaultProps = {
3435
devices: {},
36+
goToFilteredList: jest.fn(),
3537
};
3638
const getComponent = (props = {}) =>
3739
(<SecurityRecommendations {...defaultProps} {...props} />);
@@ -69,4 +71,36 @@ describe('<SecurityRecommendations />', () => {
6971
const { container } = render(getComponent({ devices }));
7072
expect(container).toMatchSnapshot();
7173
});
74+
75+
it('clicking view all unverified devices button works', () => {
76+
const goToFilteredList = jest.fn();
77+
const devices = {
78+
[verifiedNoMetadata.device_id]: verifiedNoMetadata,
79+
[hundredDaysOld.device_id]: hundredDaysOld,
80+
[unverifiedNoMetadata.device_id]: unverifiedNoMetadata,
81+
};
82+
const { getByTestId } = render(getComponent({ devices, goToFilteredList }));
83+
84+
act(() => {
85+
fireEvent.click(getByTestId('unverified-devices-cta'));
86+
});
87+
88+
expect(goToFilteredList).toHaveBeenCalledWith(DeviceSecurityVariation.Unverified);
89+
});
90+
91+
it('clicking view all inactive devices button works', () => {
92+
const goToFilteredList = jest.fn();
93+
const devices = {
94+
[verifiedNoMetadata.device_id]: verifiedNoMetadata,
95+
[hundredDaysOld.device_id]: hundredDaysOld,
96+
[unverifiedNoMetadata.device_id]: unverifiedNoMetadata,
97+
};
98+
const { getByTestId } = render(getComponent({ devices, goToFilteredList }));
99+
100+
act(() => {
101+
fireEvent.click(getByTestId('inactive-devices-cta'));
102+
});
103+
104+
expect(goToFilteredList).toHaveBeenCalledWith(DeviceSecurityVariation.Inactive);
105+
});
72106
});

test/components/views/settings/devices/__snapshots__/SecurityRecommendations-test.tsx.snap

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ exports[`<SecurityRecommendations /> renders both cards when user has both unver
4848
>
4949
<div
5050
class="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_link_inline"
51+
data-testid="unverified-devices-cta"
5152
role="button"
5253
tabindex="0"
5354
>
@@ -88,6 +89,7 @@ exports[`<SecurityRecommendations /> renders both cards when user has both unver
8889
>
8990
<div
9091
class="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_link_inline"
92+
data-testid="inactive-devices-cta"
9193
role="button"
9294
tabindex="0"
9395
>
@@ -149,6 +151,7 @@ exports[`<SecurityRecommendations /> renders inactive devices section when user
149151
>
150152
<div
151153
class="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_link_inline"
154+
data-testid="unverified-devices-cta"
152155
role="button"
153156
tabindex="0"
154157
>
@@ -189,6 +192,7 @@ exports[`<SecurityRecommendations /> renders inactive devices section when user
189192
>
190193
<div
191194
class="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_link_inline"
195+
data-testid="inactive-devices-cta"
192196
role="button"
193197
tabindex="0"
194198
>
@@ -250,6 +254,7 @@ exports[`<SecurityRecommendations /> renders unverified devices section when use
250254
>
251255
<div
252256
class="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_link_inline"
257+
data-testid="unverified-devices-cta"
253258
role="button"
254259
tabindex="0"
255260
>
@@ -290,6 +295,7 @@ exports[`<SecurityRecommendations /> renders unverified devices section when use
290295
>
291296
<div
292297
class="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_link_inline"
298+
data-testid="inactive-devices-cta"
293299
role="button"
294300
tabindex="0"
295301
>

0 commit comments

Comments
 (0)