From 951cad98d3f1507440f38bb009d6c30f4ac246b0 Mon Sep 17 00:00:00 2001
From: Kerry <kerrya@element.io>
Date: Thu, 29 Sep 2022 09:22:00 +0200
Subject: [PATCH] Device manager - extract filtered device list header (#9323)

* extract filtered device list header

* stylelint
---
 res/css/_components.pcss                      |  1 +
 .../settings/devices/_FilteredDeviceList.pcss | 20 ---------
 .../devices/_FilteredDeviceListHeader.pcss    | 36 ++++++++++++++++
 .../settings/devices/FilteredDeviceList.tsx   |  8 ++--
 .../devices/FilteredDeviceListHeader.tsx      | 42 +++++++++++++++++++
 src/i18n/strings/en_EN.json                   |  1 +
 .../devices/FilteredDeviceListHeader-test.tsx | 39 +++++++++++++++++
 .../FilteredDeviceListHeader-test.tsx.snap    | 19 +++++++++
 .../tabs/user/SessionManagerTab-test.tsx      |  2 +-
 .../SessionManagerTab-test.tsx.snap           |  4 +-
 10 files changed, 144 insertions(+), 28 deletions(-)
 create mode 100644 res/css/components/views/settings/devices/_FilteredDeviceListHeader.pcss
 create mode 100644 src/components/views/settings/devices/FilteredDeviceListHeader.tsx
 create mode 100644 test/components/views/settings/devices/FilteredDeviceListHeader-test.tsx
 create mode 100644 test/components/views/settings/devices/__snapshots__/FilteredDeviceListHeader-test.tsx.snap

diff --git a/res/css/_components.pcss b/res/css/_components.pcss
index 96e5a1a50f..6996d33cee 100644
--- a/res/css/_components.pcss
+++ b/res/css/_components.pcss
@@ -36,6 +36,7 @@
 @import "./components/views/settings/devices/_DeviceTile.pcss";
 @import "./components/views/settings/devices/_DeviceType.pcss";
 @import "./components/views/settings/devices/_FilteredDeviceList.pcss";
+@import "./components/views/settings/devices/_FilteredDeviceListHeader.pcss";
 @import "./components/views/settings/devices/_SecurityRecommendations.pcss";
 @import "./components/views/settings/devices/_SelectableDeviceTile.pcss";
 @import "./components/views/settings/shared/_SettingsSubsection.pcss";
diff --git a/res/css/components/views/settings/devices/_FilteredDeviceList.pcss b/res/css/components/views/settings/devices/_FilteredDeviceList.pcss
index 01c8df787e..4b23271225 100644
--- a/res/css/components/views/settings/devices/_FilteredDeviceList.pcss
+++ b/res/css/components/views/settings/devices/_FilteredDeviceList.pcss
@@ -20,26 +20,6 @@ limitations under the License.
     }
 }
 
-.mx_FilteredDeviceList_header {
-    display: flex;
-    flex-direction: row;
-    align-items: center;
-    box-sizing: border-box;
-
-    width: 100%;
-    height: 48px;
-    padding: 0 $spacing-16;
-    margin-bottom: $spacing-32;
-
-    background-color: $system;
-    border-radius: 8px;
-    color: $secondary-content;
-}
-
-.mx_FilteredDeviceList_headerLabel {
-    flex: 1 1 100%;
-}
-
 .mx_FilteredDeviceList_list {
     list-style-type: none;
     display: grid;
diff --git a/res/css/components/views/settings/devices/_FilteredDeviceListHeader.pcss b/res/css/components/views/settings/devices/_FilteredDeviceListHeader.pcss
new file mode 100644
index 0000000000..2cdbcf356f
--- /dev/null
+++ b/res/css/components/views/settings/devices/_FilteredDeviceListHeader.pcss
@@ -0,0 +1,36 @@
+/*
+Copyright 2022 The Matrix.org Foundation C.I.C.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+.mx_FilteredDeviceListHeader {
+    display: flex;
+    flex-direction: row;
+    align-items: center;
+    box-sizing: border-box;
+    gap: $spacing-8;
+
+    width: 100%;
+    height: 48px;
+    padding: 0 $spacing-16;
+    margin-bottom: $spacing-32;
+
+    background-color: $system;
+    border-radius: 8px;
+    color: $secondary-content;
+}
+
+.mx_FilteredDeviceListHeader_label {
+    flex: 1 1 100%;
+}
diff --git a/src/components/views/settings/devices/FilteredDeviceList.tsx b/src/components/views/settings/devices/FilteredDeviceList.tsx
index 7affee684c..5ec0a428d0 100644
--- a/src/components/views/settings/devices/FilteredDeviceList.tsx
+++ b/src/components/views/settings/devices/FilteredDeviceList.tsx
@@ -36,6 +36,7 @@ import {
     DeviceWithVerification,
 } from './types';
 import { DevicesState } from './useOwnDevices';
+import FilteredDeviceListHeader from './FilteredDeviceListHeader';
 
 interface Props {
     devices: DevicesDictionary;
@@ -242,10 +243,7 @@ export const FilteredDeviceList =
         };
 
         return <div className='mx_FilteredDeviceList' ref={ref}>
-            <div className='mx_FilteredDeviceList_header'>
-                <span className='mx_FilteredDeviceList_headerLabel'>
-                    { _t('Sessions') }
-                </span>
+            <FilteredDeviceListHeader selectedDeviceCount={0}>
                 <FilterDropdown<DeviceFilterKey>
                     id='device-list-filter'
                     label={_t('Filter devices')}
@@ -254,7 +252,7 @@ export const FilteredDeviceList =
                     options={options}
                     selectedLabel={_t('Show')}
                 />
-            </div>
+            </FilteredDeviceListHeader>
             { !!sortedDevices.length
                 ? <FilterSecurityCard filter={filter} />
                 : <NoResults filter={filter} clearFilter={() => onFilterChange(undefined)} />
diff --git a/src/components/views/settings/devices/FilteredDeviceListHeader.tsx b/src/components/views/settings/devices/FilteredDeviceListHeader.tsx
new file mode 100644
index 0000000000..6ecee4c8d2
--- /dev/null
+++ b/src/components/views/settings/devices/FilteredDeviceListHeader.tsx
@@ -0,0 +1,42 @@
+/*
+Copyright 2022 The Matrix.org Foundation C.I.C.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+import React, { HTMLProps } from 'react';
+
+import { _t } from '../../../../languageHandler';
+
+interface Props extends Omit<HTMLProps<HTMLDivElement>, 'className'> {
+    selectedDeviceCount: number;
+    children?: React.ReactNode;
+}
+
+const FilteredDeviceListHeader: React.FC<Props> = ({
+    selectedDeviceCount,
+    children,
+    ...rest
+}) => {
+    return <div className='mx_FilteredDeviceListHeader' {...rest}>
+        <span className='mx_FilteredDeviceListHeader_label'>
+            { selectedDeviceCount > 0
+                ? _t('%(selectedDeviceCount)s sessions selected', { selectedDeviceCount })
+                : _t('Sessions')
+            }
+        </span>
+        { children }
+    </div>;
+};
+
+export default FilteredDeviceListHeader;
diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json
index b8a7361175..c1b25cb2ab 100644
--- a/src/i18n/strings/en_EN.json
+++ b/src/i18n/strings/en_EN.json
@@ -1753,6 +1753,7 @@
     "Inactive for %(inactiveAgeDays)s days or longer": "Inactive for %(inactiveAgeDays)s days or longer",
     "Filter devices": "Filter devices",
     "Show": "Show",
+    "%(selectedDeviceCount)s sessions selected": "%(selectedDeviceCount)s sessions selected",
     "Security recommendations": "Security recommendations",
     "Improve your account security by following these recommendations": "Improve your account security by following these recommendations",
     "View all": "View all",
diff --git a/test/components/views/settings/devices/FilteredDeviceListHeader-test.tsx b/test/components/views/settings/devices/FilteredDeviceListHeader-test.tsx
new file mode 100644
index 0000000000..8f7ecdc924
--- /dev/null
+++ b/test/components/views/settings/devices/FilteredDeviceListHeader-test.tsx
@@ -0,0 +1,39 @@
+/*
+Copyright 2022 The Matrix.org Foundation C.I.C.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+import { render } from '@testing-library/react';
+import React from 'react';
+
+import FilteredDeviceListHeader from '../../../../../src/components/views/settings/devices/FilteredDeviceListHeader';
+
+describe('<FilteredDeviceListHeader />', () => {
+    const defaultProps = {
+        selectedDeviceCount: 0,
+        children: <div>test</div>,
+        ['data-testid']: 'test123',
+    };
+    const getComponent = (props = {}) => (<FilteredDeviceListHeader {...defaultProps} {...props} />);
+
+    it('renders correctly when no devices are selected', () => {
+        const { container } = render(getComponent());
+        expect(container).toMatchSnapshot();
+    });
+
+    it('renders correctly when some devices are selected', () => {
+        const { getByText } = render(getComponent({ selectedDeviceCount: 2 }));
+        expect(getByText('2 sessions selected')).toBeTruthy();
+    });
+});
diff --git a/test/components/views/settings/devices/__snapshots__/FilteredDeviceListHeader-test.tsx.snap b/test/components/views/settings/devices/__snapshots__/FilteredDeviceListHeader-test.tsx.snap
new file mode 100644
index 0000000000..f474cca811
--- /dev/null
+++ b/test/components/views/settings/devices/__snapshots__/FilteredDeviceListHeader-test.tsx.snap
@@ -0,0 +1,19 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`<FilteredDeviceListHeader /> renders correctly when no devices are selected 1`] = `
+<div>
+  <div
+    class="mx_FilteredDeviceListHeader"
+    data-testid="test123"
+  >
+    <span
+      class="mx_FilteredDeviceListHeader_label"
+    >
+      Sessions
+    </span>
+    <div>
+      test
+    </div>
+  </div>
+</div>
+`;
diff --git a/test/components/views/settings/tabs/user/SessionManagerTab-test.tsx b/test/components/views/settings/tabs/user/SessionManagerTab-test.tsx
index 5c1d5586aa..70bb171b12 100644
--- a/test/components/views/settings/tabs/user/SessionManagerTab-test.tsx
+++ b/test/components/views/settings/tabs/user/SessionManagerTab-test.tsx
@@ -279,7 +279,7 @@ describe('<SessionManagerTab />', () => {
         await flushPromisesWithFakeTimers();
 
         // unverified filter is set
-        expect(container.querySelector('.mx_FilteredDeviceList_header')).toMatchSnapshot();
+        expect(container.querySelector('.mx_FilteredDeviceListHeader')).toMatchSnapshot();
     });
 
     describe('device detail expansion', () => {
diff --git a/test/components/views/settings/tabs/user/__snapshots__/SessionManagerTab-test.tsx.snap b/test/components/views/settings/tabs/user/__snapshots__/SessionManagerTab-test.tsx.snap
index f4f120d0a5..32af1a3fa6 100644
--- a/test/components/views/settings/tabs/user/__snapshots__/SessionManagerTab-test.tsx.snap
+++ b/test/components/views/settings/tabs/user/__snapshots__/SessionManagerTab-test.tsx.snap
@@ -17,10 +17,10 @@ exports[`<SessionManagerTab /> Sign out Signs out of current device 1`] = `
 
 exports[`<SessionManagerTab /> goes to filtered list from security recommendations 1`] = `
 <div
-  class="mx_FilteredDeviceList_header"
+  class="mx_FilteredDeviceListHeader"
 >
   <span
-    class="mx_FilteredDeviceList_headerLabel"
+    class="mx_FilteredDeviceListHeader_label"
   >
     Sessions
   </span>