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

Commit 776ffa4

Browse files
author
Kerry
authored
Device manager - current session context menu (#9386)
* add destructive option and close on interaction options * add kebab context menu wrapper * use kebab context menu in current device section * use named export * lint * sessionman tests
1 parent 8b54be6 commit 776ffa4

File tree

14 files changed

+467
-107
lines changed

14 files changed

+467
-107
lines changed

res/css/_components.pcss

+1
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
@import "./components/views/beacon/_RoomLiveShareWarning.pcss";
1818
@import "./components/views/beacon/_ShareLatestLocation.pcss";
1919
@import "./components/views/beacon/_StyledLiveBeaconIcon.pcss";
20+
@import "./components/views/context_menus/_KebabContextMenu.pcss";
2021
@import "./components/views/elements/_FilterDropdown.pcss";
2122
@import "./components/views/location/_EnableLiveShare.pcss";
2223
@import "./components/views/location/_LiveDurationDropdown.pcss";
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
/*
2+
Copyright 2022 The Matrix.org Foundation C.I.C.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
.mx_KebabContextMenu_icon {
18+
width: 24px;
19+
color: $secondary-content;
20+
}

res/css/views/context_menus/_IconizedContextMenu.pcss

+6-1
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,8 @@ limitations under the License.
8282
display: flex;
8383
align-items: center;
8484

85-
&:hover {
85+
&:hover,
86+
&:focus {
8687
background-color: $menu-selected-color;
8788
}
8889

@@ -187,3 +188,7 @@ limitations under the License.
187188
color: $tertiary-content;
188189
}
189190
}
191+
192+
.mx_IconizedContextMenu_item.mx_IconizedContextMenu_itemDestructive {
193+
color: $alert !important;
194+
}

src/components/structures/ContextMenu.tsx

+7
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,9 @@ export interface IProps extends IPosition {
9292
// within an existing FocusLock e.g inside a modal.
9393
focusLock?: boolean;
9494

95+
// call onFinished on any interaction with the menu
96+
closeOnInteraction?: boolean;
97+
9598
// Function to be called on menu close
9699
onFinished();
97100
// on resize callback
@@ -186,6 +189,10 @@ export default class ContextMenu extends React.PureComponent<IProps, IState> {
186189
private onClick = (ev: React.MouseEvent) => {
187190
// Don't allow clicks to escape the context menu wrapper
188191
ev.stopPropagation();
192+
193+
if (this.props.closeOnInteraction) {
194+
this.props.onFinished?.();
195+
}
189196
};
190197

191198
// We now only handle closing the ContextMenu in this keyDown handler.

src/components/views/context_menus/IconizedContextMenu.tsx

+3
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ interface IOptionListProps {
3939

4040
interface IOptionProps extends React.ComponentProps<typeof MenuItem> {
4141
iconClassName?: string;
42+
isDestructive?: boolean;
4243
}
4344

4445
interface ICheckboxProps extends React.ComponentProps<typeof MenuItemCheckbox> {
@@ -112,12 +113,14 @@ export const IconizedContextMenuOption: React.FC<IOptionProps> = ({
112113
className,
113114
iconClassName,
114115
children,
116+
isDestructive,
115117
...props
116118
}) => {
117119
return <MenuItem
118120
{...props}
119121
className={classNames(className, {
120122
mx_IconizedContextMenu_item: true,
123+
mx_IconizedContextMenu_itemDestructive: isDestructive,
121124
})}
122125
label={label}
123126
>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
/*
2+
Copyright 2022 The Matrix.org Foundation C.I.C.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
import React from 'react';
18+
19+
import { Icon as ContextMenuIcon } from '../../../../res/img/element-icons/context-menu.svg';
20+
import { ChevronFace, ContextMenuButton, useContextMenu } from '../../structures/ContextMenu';
21+
import AccessibleButton from '../elements/AccessibleButton';
22+
import IconizedContextMenu, { IconizedContextMenuOptionList } from './IconizedContextMenu';
23+
24+
const contextMenuBelow = (elementRect: DOMRect) => {
25+
// align the context menu's icons with the icon which opened the context menu
26+
const left = elementRect.left + window.scrollX + elementRect.width;
27+
const top = elementRect.bottom + window.scrollY;
28+
const chevronFace = ChevronFace.None;
29+
return { left, top, chevronFace };
30+
};
31+
32+
interface KebabContextMenuProps extends Partial<React.ComponentProps<typeof AccessibleButton>> {
33+
options: React.ReactNode[];
34+
title: string;
35+
}
36+
37+
export const KebabContextMenu: React.FC<KebabContextMenuProps> = ({
38+
options,
39+
title,
40+
...props
41+
}) => {
42+
const [menuDisplayed, button, openMenu, closeMenu] = useContextMenu();
43+
44+
return <>
45+
<ContextMenuButton
46+
{...props}
47+
onClick={openMenu}
48+
title={title}
49+
isExpanded={menuDisplayed}
50+
inputRef={button}
51+
>
52+
<ContextMenuIcon className='mx_KebabContextMenu_icon' />
53+
</ContextMenuButton>
54+
{ menuDisplayed && (<IconizedContextMenu
55+
onFinished={closeMenu}
56+
compact
57+
rightAligned
58+
closeOnInteraction
59+
{...contextMenuBelow(button.current.getBoundingClientRect())}
60+
>
61+
<IconizedContextMenuOptionList>
62+
{ options }
63+
</IconizedContextMenuOptionList>
64+
</IconizedContextMenu>) }
65+
</>;
66+
};

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

+49-2
Original file line numberDiff line numberDiff line change
@@ -14,17 +14,20 @@ See the License for the specific language governing permissions and
1414
limitations under the License.
1515
*/
1616

17-
import { LocalNotificationSettings } from 'matrix-js-sdk/src/@types/local_notifications';
1817
import React, { useState } from 'react';
18+
import { LocalNotificationSettings } from 'matrix-js-sdk/src/@types/local_notifications';
1919

2020
import { _t } from '../../../../languageHandler';
2121
import Spinner from '../../elements/Spinner';
2222
import SettingsSubsection from '../shared/SettingsSubsection';
23+
import { SettingsSubsectionHeading } from '../shared/SettingsSubsectionHeading';
2324
import DeviceDetails from './DeviceDetails';
2425
import DeviceExpandDetailsButton from './DeviceExpandDetailsButton';
2526
import DeviceTile from './DeviceTile';
2627
import { DeviceVerificationStatusCard } from './DeviceVerificationStatusCard';
2728
import { ExtendedDevice } from './types';
29+
import { KebabContextMenu } from '../../context_menus/KebabContextMenu';
30+
import { IconizedContextMenuOption } from '../../context_menus/IconizedContextMenu';
2831

2932
interface Props {
3033
device?: ExtendedDevice;
@@ -34,9 +37,48 @@ interface Props {
3437
setPushNotifications?: (deviceId: string, enabled: boolean) => Promise<void> | undefined;
3538
onVerifyCurrentDevice: () => void;
3639
onSignOutCurrentDevice: () => void;
40+
signOutAllOtherSessions?: () => void;
3741
saveDeviceName: (deviceName: string) => Promise<void>;
3842
}
3943

44+
type CurrentDeviceSectionHeadingProps =
45+
Pick<Props, 'onSignOutCurrentDevice' | 'signOutAllOtherSessions'>
46+
& { disabled?: boolean };
47+
48+
const CurrentDeviceSectionHeading: React.FC<CurrentDeviceSectionHeadingProps> = ({
49+
onSignOutCurrentDevice,
50+
signOutAllOtherSessions,
51+
disabled,
52+
}) => {
53+
const menuOptions = [
54+
<IconizedContextMenuOption
55+
key="sign-out"
56+
label={_t('Sign out')}
57+
onClick={onSignOutCurrentDevice}
58+
isDestructive
59+
/>,
60+
...(signOutAllOtherSessions
61+
? [
62+
<IconizedContextMenuOption
63+
key="sign-out-all-others"
64+
label={_t('Sign out all other sessions')}
65+
onClick={signOutAllOtherSessions}
66+
isDestructive
67+
/>,
68+
]
69+
: []
70+
),
71+
];
72+
return <SettingsSubsectionHeading heading={_t('Current session')}>
73+
<KebabContextMenu
74+
disabled={disabled}
75+
title={_t('Options')}
76+
options={menuOptions}
77+
data-testid='current-session-menu'
78+
/>
79+
</SettingsSubsectionHeading>;
80+
};
81+
4082
const CurrentDeviceSection: React.FC<Props> = ({
4183
device,
4284
isLoading,
@@ -45,13 +87,18 @@ const CurrentDeviceSection: React.FC<Props> = ({
4587
setPushNotifications,
4688
onVerifyCurrentDevice,
4789
onSignOutCurrentDevice,
90+
signOutAllOtherSessions,
4891
saveDeviceName,
4992
}) => {
5093
const [isExpanded, setIsExpanded] = useState(false);
5194

5295
return <SettingsSubsection
53-
heading={_t('Current session')}
5496
data-testid='current-session-section'
97+
heading={<CurrentDeviceSectionHeading
98+
onSignOutCurrentDevice={onSignOutCurrentDevice}
99+
signOutAllOtherSessions={signOutAllOtherSessions}
100+
disabled={isLoading || !device || isSigningOut}
101+
/>}
55102
>
56103
{ /* only show big spinner on first load */ }
57104
{ isLoading && !device && <Spinner /> }

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

+5
Original file line numberDiff line numberDiff line change
@@ -171,6 +171,10 @@ const SessionManagerTab: React.FC = () => {
171171
setSelectedDeviceIds([]);
172172
}, [filter, setSelectedDeviceIds]);
173173

174+
const signOutAllOtherSessions = shouldShowOtherSessions ? () => {
175+
onSignOutOtherDevices(Object.keys(otherDevices));
176+
}: undefined;
177+
174178
return <SettingsTab heading={_t('Sessions')}>
175179
<SecurityRecommendations
176180
devices={devices}
@@ -186,6 +190,7 @@ const SessionManagerTab: React.FC = () => {
186190
saveDeviceName={(deviceName) => saveDeviceName(currentDeviceId, deviceName)}
187191
onVerifyCurrentDevice={onVerifyCurrentDevice}
188192
onSignOutCurrentDevice={onSignOutCurrentDevice}
193+
signOutAllOtherSessions={signOutAllOtherSessions}
189194
/>
190195
{
191196
shouldShowOtherSessions &&

src/i18n/strings/en_EN.json

+2-1
Original file line numberDiff line numberDiff line change
@@ -1718,6 +1718,8 @@
17181718
"Please enter verification code sent via text.": "Please enter verification code sent via text.",
17191719
"Verification code": "Verification code",
17201720
"Discovery options will appear once you have added a phone number above.": "Discovery options will appear once you have added a phone number above.",
1721+
"Sign out": "Sign out",
1722+
"Sign out all other sessions": "Sign out all other sessions",
17211723
"Current session": "Current session",
17221724
"Confirm logging out these devices by using Single Sign On to prove your identity.|other": "Confirm logging out these devices by using Single Sign On to prove your identity.",
17231725
"Confirm logging out these devices by using Single Sign On to prove your identity.|one": "Confirm logging out this device by using Single Sign On to prove your identity.",
@@ -1774,7 +1776,6 @@
17741776
"Not ready for secure messaging": "Not ready for secure messaging",
17751777
"Inactive": "Inactive",
17761778
"Inactive for %(inactiveAgeDays)s days or longer": "Inactive for %(inactiveAgeDays)s days or longer",
1777-
"Sign out": "Sign out",
17781779
"Filter devices": "Filter devices",
17791780
"Show": "Show",
17801781
"%(selectedDeviceCount)s sessions selected": "%(selectedDeviceCount)s sessions selected",

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

+38
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,20 @@ exports[`<CurrentDeviceSection /> handles when device is falsy 1`] = `
128128
>
129129
Current session
130130
</h3>
131+
<div
132+
aria-disabled="true"
133+
aria-expanded="false"
134+
aria-haspopup="true"
135+
class="mx_AccessibleButton mx_AccessibleButton_disabled"
136+
data-testid="current-session-menu"
137+
disabled=""
138+
role="button"
139+
tabindex="0"
140+
>
141+
<div
142+
class="mx_KebabContextMenu_icon"
143+
/>
144+
</div>
131145
</div>
132146
<div
133147
class="mx_SettingsSubsection_content"
@@ -150,6 +164,18 @@ exports[`<CurrentDeviceSection /> renders device and correct security card when
150164
>
151165
Current session
152166
</h3>
167+
<div
168+
aria-expanded="false"
169+
aria-haspopup="true"
170+
class="mx_AccessibleButton"
171+
data-testid="current-session-menu"
172+
role="button"
173+
tabindex="0"
174+
>
175+
<div
176+
class="mx_KebabContextMenu_icon"
177+
/>
178+
</div>
153179
</div>
154180
<div
155181
class="mx_SettingsSubsection_content"
@@ -274,6 +300,18 @@ exports[`<CurrentDeviceSection /> renders device and correct security card when
274300
>
275301
Current session
276302
</h3>
303+
<div
304+
aria-expanded="false"
305+
aria-haspopup="true"
306+
class="mx_AccessibleButton"
307+
data-testid="current-session-menu"
308+
role="button"
309+
tabindex="0"
310+
>
311+
<div
312+
class="mx_KebabContextMenu_icon"
313+
/>
314+
</div>
277315
</div>
278316
<div
279317
class="mx_SettingsSubsection_content"
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
/*
2+
Copyright 2022 The Matrix.org Foundation C.I.C.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
import { render } from '@testing-library/react';
18+
import React from 'react';
19+
20+
import {
21+
SettingsSubsectionHeading,
22+
} from '../../../../../src/components/views/settings/shared/SettingsSubsectionHeading';
23+
24+
describe('<SettingsSubsectionHeading />', () => {
25+
const defaultProps = {
26+
heading: 'test',
27+
};
28+
const getComponent = (props = {}) =>
29+
render(<SettingsSubsectionHeading {...defaultProps} {...props} />);
30+
31+
it('renders without children', () => {
32+
const { container } = getComponent();
33+
expect({ container }).toMatchSnapshot();
34+
});
35+
36+
it('renders with children', () => {
37+
const children = <a href='/#'>test</a>;
38+
const { container } = getComponent({ children });
39+
expect({ container }).toMatchSnapshot();
40+
});
41+
});

0 commit comments

Comments
 (0)