Skip to content

Commit 5196425

Browse files
darkwingsalimtb
authored andcommitted
Add NetworkListItemMenu
1 parent 59d736c commit 5196425

File tree

13 files changed

+435
-312
lines changed

13 files changed

+435
-312
lines changed

app/_locales/en/messages.json

+3
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

ui/components/multichain/multichain-components.scss

+1
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
@import 'connected-site-menu';
2020
@import 'token-list-item';
2121
@import 'network-list-item';
22+
@import 'network-list-item-menu';
2223
@import 'network-list-menu';
2324
@import 'product-tour-popover';
2425
@import 'nft-item';
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export { NetworkListItemMenu } from './network-list-item-menu';
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
@use "design-system";
2+
3+
.multichain-network-list-item-menu__popover {
4+
z-index: design-system.$popover-in-modal-z-index;
5+
overflow: hidden;
6+
min-width: 225px;
7+
max-width: 225px;
8+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
import React, { useCallback, useEffect, useRef } from 'react';
2+
import PropTypes from 'prop-types';
3+
import { useI18nContext } from '../../../hooks/useI18nContext';
4+
import {
5+
IconName,
6+
ModalFocus,
7+
Popover,
8+
PopoverPosition,
9+
PopoverRole,
10+
Text,
11+
} from '../../component-library';
12+
import { MenuItem } from '../../ui/menu';
13+
import { IconColor, TextColor } from '../../../helpers/constants/design-system';
14+
15+
export const NetworkListItemMenu = ({
16+
anchorElement,
17+
onClose,
18+
onEditClick,
19+
onDeleteClick,
20+
isOpen,
21+
}) => {
22+
const t = useI18nContext();
23+
24+
// Handle Tab key press for accessibility inside the popover and will close the popover on the last MenuItem
25+
const lastItemRef = useRef(null);
26+
const accountDetailsItemRef = useRef(null);
27+
const removeAccountItemRef = useRef(null);
28+
const removeJWTItemRef = useRef(null);
29+
30+
// Checks the MenuItems from the bottom to top to set lastItemRef on the last MenuItem that is not disabled
31+
useEffect(() => {
32+
if (removeJWTItemRef.current) {
33+
lastItemRef.current = removeJWTItemRef.current;
34+
} else if (removeAccountItemRef.current) {
35+
lastItemRef.current = removeAccountItemRef.current;
36+
} else {
37+
lastItemRef.current = accountDetailsItemRef.current;
38+
}
39+
// eslint-disable-next-line react-hooks/exhaustive-deps
40+
}, [
41+
removeJWTItemRef.current,
42+
removeAccountItemRef.current,
43+
accountDetailsItemRef.current,
44+
]);
45+
46+
const handleKeyDown = useCallback(
47+
(event) => {
48+
if (event.key === 'Tab' && event.target === lastItemRef.current) {
49+
// If Tab is pressed at the last item to close popover and focus to next element in DOM
50+
onClose();
51+
}
52+
},
53+
[onClose],
54+
);
55+
56+
// Handle click outside of the popover to close it
57+
const popoverDialogRef = useRef(null);
58+
59+
const handleClickOutside = useCallback(
60+
(event) => {
61+
if (
62+
popoverDialogRef?.current &&
63+
!popoverDialogRef.current.contains(event.target)
64+
) {
65+
onClose();
66+
}
67+
},
68+
[onClose],
69+
);
70+
71+
useEffect(() => {
72+
document.addEventListener('mousedown', handleClickOutside);
73+
74+
return () => {
75+
document.removeEventListener('mousedown', handleClickOutside);
76+
};
77+
}, [handleClickOutside]);
78+
79+
return (
80+
<Popover
81+
className="multichain-network-list-item-menu__popover"
82+
referenceElement={anchorElement}
83+
role={PopoverRole.Dialog}
84+
position={PopoverPosition.Bottom}
85+
offset={[0, 0]}
86+
padding={0}
87+
isOpen={isOpen}
88+
isPortal
89+
preventOverflow
90+
flip
91+
>
92+
<ModalFocus restoreFocus initialFocusRef={anchorElement}>
93+
<div onKeyDown={handleKeyDown} ref={popoverDialogRef}>
94+
{onEditClick ? (
95+
<MenuItem
96+
iconName={IconName.Edit}
97+
onClick={(e) => {
98+
e.stopPropagation();
99+
100+
// Pass network info?
101+
onEditClick();
102+
}}
103+
data-testid="network-list-item-options-edit"
104+
>
105+
{t('edit')}
106+
</MenuItem>
107+
) : null}
108+
{onDeleteClick ? (
109+
<MenuItem
110+
iconName={IconName.Trash}
111+
iconColor={IconColor.errorDefault}
112+
onClick={(e) => {
113+
e.stopPropagation();
114+
115+
// Pass network info?
116+
onDeleteClick();
117+
}}
118+
data-testid="network-list-item-options-delete"
119+
>
120+
<Text color={TextColor.errorDefault}>{t('delete')}</Text>
121+
</MenuItem>
122+
) : null}
123+
</div>
124+
</ModalFocus>
125+
</Popover>
126+
);
127+
};
128+
129+
NetworkListItemMenu.propTypes = {
130+
/**
131+
* Element that the menu should display next to
132+
*/
133+
anchorElement: PropTypes.instanceOf(window.Element),
134+
/**
135+
* Function that executes when the menu is closed
136+
*/
137+
onClose: PropTypes.func.isRequired,
138+
/**
139+
* Function that executes when the Edit menu item is clicked
140+
*/
141+
onEditClick: PropTypes.func,
142+
/**
143+
* Function that executes when the Delete menu item is closed
144+
*/
145+
onDeleteClick: PropTypes.func,
146+
/**
147+
* Represents if the menu is open or not
148+
*
149+
* @type {boolean}
150+
*/
151+
isOpen: PropTypes.bool.isRequired,
152+
};

ui/components/multichain/network-list-item/__snapshots__/network-list-item.test.js.snap

+4-3
Original file line numberDiff line numberDiff line change
@@ -26,12 +26,13 @@ exports[`NetworkListItem renders properly 1`] = `
2626
</p>
2727
</div>
2828
<button
29-
aria-label="[deleteNetwork]"
30-
class="mm-box mm-button-icon mm-button-icon--size-sm multichain-network-list-item__delete mm-box--display-inline-flex mm-box--justify-content-center mm-box--align-items-center mm-box--color-error-default mm-box--background-color-transparent mm-box--rounded-lg"
29+
aria-label="[networkOptions]"
30+
class="mm-box mm-button-icon mm-button-icon--size-sm mm-box--display-inline-flex mm-box--justify-content-center mm-box--align-items-center mm-box--color-icon-default mm-box--background-color-transparent mm-box--rounded-lg"
31+
data-testid="network-list-item-options-button"
3132
>
3233
<span
3334
class="mm-box mm-icon mm-icon--size-sm mm-box--display-inline-block mm-box--color-inherit"
34-
style="mask-image: url('./images/icons/trash.svg');"
35+
style="mask-image: url('./images/icons/more-vertical.svg');"
3536
/>
3637
</button>
3738
</div>

ui/components/multichain/network-list-item/network-list-item.js

+15-71
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import React, { useCallback, useEffect, useRef, useState } from 'react';
1+
import React, { useEffect, useRef, useState } from 'react';
22
import classnames from 'classnames';
33
import PropTypes from 'prop-types';
44
import {
@@ -8,7 +8,6 @@ import {
88
BorderRadius,
99
Color,
1010
Display,
11-
IconColor,
1211
JustifyContent,
1312
TextColor,
1413
} from '../../../helpers/constants/design-system';
@@ -18,15 +17,12 @@ import {
1817
ButtonIcon,
1918
ButtonIconSize,
2019
IconName,
21-
Popover,
22-
PopoverPosition,
23-
PopoverRole,
2420
Text,
2521
} from '../../component-library';
2622
import { useI18nContext } from '../../../hooks/useI18nContext';
2723
import { getAvatarNetworkColor } from '../../../helpers/utils/accounts';
2824
import Tooltip from '../../ui/tooltip/tooltip';
29-
import { MenuItem } from '../../ui/menu';
25+
import { NetworkListItemMenu } from '../network-list-item-menu';
3026

3127
const MAXIMUM_CHARACTERS_WITHOUT_TOOLTIP = 20;
3228

@@ -41,32 +37,14 @@ export const NetworkListItem = ({
4137
}) => {
4238
const t = useI18nContext();
4339
const networkRef = useRef();
44-
const menuRef = useRef(null);
4540

46-
const [networkOptionsMenuOpen, setNetworkOptionsMenuOpen] = useState(false);
47-
48-
// Handle click outside of the popover to close it
49-
const popoverDialogRef = useRef(null);
50-
51-
const handleClickOutside = useCallback(
52-
(event) => {
53-
if (
54-
popoverDialogRef?.current &&
55-
!popoverDialogRef.current.contains(event.target)
56-
) {
57-
setNetworkOptionsMenuOpen(false);
58-
}
59-
},
60-
[setNetworkOptionsMenuOpen],
61-
);
62-
63-
useEffect(() => {
64-
document.addEventListener('mousedown', handleClickOutside);
41+
const [networkListItemMenuElement, setNetworkListItemMenuElement] =
42+
useState();
43+
const setNetworkListItemMenuRef = (ref) => {
44+
setNetworkListItemMenuElement(ref);
45+
};
6546

66-
return () => {
67-
document.removeEventListener('mousedown', handleClickOutside);
68-
};
69-
}, [handleClickOutside]);
47+
const [networkOptionsMenuOpen, setNetworkOptionsMenuOpen] = useState(false);
7048

7149
useEffect(() => {
7250
if (networkRef.current && focus) {
@@ -137,57 +115,23 @@ export const NetworkListItem = ({
137115
{onDeleteClick || onEditClick ? (
138116
<ButtonIcon
139117
iconName={IconName.MoreVertical}
118+
ref={setNetworkListItemMenuRef}
140119
data-testid="network-list-item-options-button"
141-
ariaLabel="Network options"
142-
ref={menuRef}
120+
ariaLabel={t('networkOptions')}
143121
onClick={(e) => {
144-
e.preventDefault();
145122
e.stopPropagation();
146-
147-
console.log(`Opening menu for ${name}`);
148123
setNetworkOptionsMenuOpen(true);
149124
}}
150125
size={ButtonIconSize.Sm}
151126
/>
152127
) : null}
153-
<Popover
128+
<NetworkListItemMenu
129+
anchorElement={networkListItemMenuElement}
154130
isOpen={networkOptionsMenuOpen}
131+
onDeleteClick={onDeleteClick}
132+
onEditClick={onEditClick}
155133
onClose={() => setNetworkOptionsMenuOpen(false)}
156-
role={PopoverRole.Dialog}
157-
position={PopoverPosition.Bottom}
158-
offset={[0, 0]}
159-
>
160-
{onEditClick ? (
161-
<MenuItem
162-
iconName={IconName.Edit}
163-
onClick={(e) => {
164-
e.stopPropagation();
165-
166-
// Pass network info?
167-
onEditClick();
168-
}}
169-
data-testid="network-list-item-options-edit"
170-
>
171-
{t('edit')}
172-
</MenuItem>
173-
) : null}
174-
{onDeleteClick ? (
175-
<MenuItem
176-
iconName={IconName.Trash}
177-
iconColor={IconColor.errorDefault}
178-
onClick={(e) => {
179-
e.stopPropagation();
180-
181-
// Pass network info?
182-
onDeleteClick();
183-
}}
184-
data-testid="network-list-item-options-delete"
185-
color={TextColor.errorDefault}
186-
>
187-
{t('delete')}
188-
</MenuItem>
189-
) : null}
190-
</Popover>
134+
/>
191135
</Box>
192136
);
193137
};

ui/components/multichain/network-list-item/network-list-item.test.js

+6-4
Original file line numberDiff line numberDiff line change
@@ -65,16 +65,18 @@ describe('NetworkListItem', () => {
6565
it('executes onDeleteClick when the delete button is clicked', () => {
6666
const onDeleteClick = jest.fn();
6767
const onClick = jest.fn();
68-
const { container } = render(
68+
69+
const { getByTestId } = render(
6970
<NetworkListItem
7071
{...DEFAULT_PROPS}
7172
onDeleteClick={onDeleteClick}
7273
onClick={onClick}
7374
/>,
7475
);
75-
fireEvent.click(
76-
container.querySelector('.multichain-network-list-item__delete'),
77-
);
76+
77+
fireEvent.click(getByTestId('network-list-item-options-button'));
78+
79+
fireEvent.click(getByTestId('network-list-item-options-delete'));
7880
expect(onDeleteClick).toHaveBeenCalledTimes(1);
7981
expect(onClick).toHaveBeenCalledTimes(0);
8082
});

0 commit comments

Comments
 (0)