Skip to content

Commit 054938d

Browse files
Merge pull request #714 from INNOVATIVEGAMER/Support-for-external-link-in-MenuItem
Added support for external link in simple menu
2 parents b30d2bd + 9b7b4f2 commit 054938d

File tree

7 files changed

+200
-82
lines changed

7 files changed

+200
-82
lines changed

packages/strapi-design-system/src/Link/Link.js

+6-3
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import React from 'react';
1+
import React, { forwardRef } from 'react';
22
import PropTypes from 'prop-types';
33
import styled from 'styled-components';
44
import ExternalLink from '@strapi/icons/ExternalLink';
@@ -43,7 +43,7 @@ const IconWrapper = styled(Box)`
4343
display: flex;
4444
`;
4545

46-
export const Link = ({ href, to, children, disabled, startIcon, endIcon, ...props }) => {
46+
export const Link = forwardRef(({ href, to, children, disabled, startIcon, endIcon, ...props }, ref) => {
4747
const target = href ? '_blank' : undefined;
4848
const rel = href ? 'noreferrer noopener' : undefined;
4949

@@ -55,13 +55,15 @@ export const Link = ({ href, to, children, disabled, startIcon, endIcon, ...prop
5555
to={disabled ? undefined : to}
5656
href={disabled ? '#' : href}
5757
disabled={disabled}
58+
ref={ref}
5859
{...props}
5960
>
6061
{startIcon && (
6162
<IconWrapper as="span" aria-hidden paddingRight={2}>
6263
{startIcon}
6364
</IconWrapper>
6465
)}
66+
6567
<Typography>{children}</Typography>
6668

6769
{endIcon && !href && (
@@ -77,7 +79,7 @@ export const Link = ({ href, to, children, disabled, startIcon, endIcon, ...prop
7779
)}
7880
</LinkWrapper>
7981
);
80-
};
82+
});
8183

8284
Link.displayName = 'Link';
8385

@@ -101,6 +103,7 @@ Link.propTypes = {
101103
// eslint-disable-next-line consistent-return
102104
return undefined;
103105
},
106+
104107
startIcon: PropTypes.element,
105108
to(props) {
106109
if (!props.disabled && !props.href && !props.to) {

packages/strapi-design-system/src/SimpleMenu/SimpleMenu.js

+44-10
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,10 @@ import PropTypes from 'prop-types';
33
import styled from 'styled-components';
44
import { NavLink } from 'react-router-dom';
55
import { useCallbackRef } from '@radix-ui/react-use-callback-ref';
6-
76
import CarretDown from '@strapi/icons/CarretDown';
87

98
import { Typography } from '../Typography';
9+
import { Link } from '../Link';
1010
import { Box } from '../Box';
1111
import { Flex } from '../Flex';
1212
import { Button } from '../Button';
@@ -28,6 +28,24 @@ const OptionLink = styled(NavLink)`
2828
${getOptionStyle}
2929
`;
3030

31+
const OptionExternalLink = styled(Link)`
32+
/* Removing Link hover effect */
33+
&:hover {
34+
color: currentColor;
35+
}
36+
37+
&:focus-visible {
38+
/* Removing Link focus-visible after properties and reset to global outline */
39+
outline: 2px solid ${({ theme }) => theme.colors.primary600};
40+
outline-offset: 2px;
41+
&:after {
42+
content: none;
43+
}
44+
}
45+
46+
${getOptionStyle}
47+
`;
48+
3149
const IconWrapper = styled.span`
3250
display: flex;
3351
align-items: center;
@@ -41,7 +59,7 @@ const StyledButtonSmall = styled(Button)`
4159
padding: ${({ theme }) => `${theme.spaces[1]} ${theme.spaces[3]}`};
4260
`;
4361

44-
export const MenuItem = ({ children, onClick, to, isFocused, ...props }) => {
62+
export const MenuItem = ({ children, onClick, to, isFocused, href, ...props }) => {
4563
const menuItemRef = useRef();
4664

4765
useEffect(() => {
@@ -63,13 +81,27 @@ export const MenuItem = ({ children, onClick, to, isFocused, ...props }) => {
6381
}
6482
};
6583

66-
return to ? (
67-
<OptionLink to={to} {...menuItemProps}>
68-
<Box padding={2}>
69-
<Typography>{children}</Typography>
70-
</Box>
71-
</OptionLink>
72-
) : (
84+
if (to && !href) {
85+
return (
86+
<OptionLink to={to} {...menuItemProps}>
87+
<Box padding={2}>
88+
<Typography>{children}</Typography>
89+
</Box>
90+
</OptionLink>
91+
);
92+
}
93+
94+
if (href && !to) {
95+
return (
96+
<OptionExternalLink isExternal href={href} {...menuItemProps}>
97+
<Box padding={2}>
98+
<Typography>{children}</Typography>
99+
</Box>
100+
</OptionExternalLink>
101+
);
102+
}
103+
104+
return (
73105
<OptionButton onKeyDown={handleKeyDown} onMouseDown={onClick} type="button" {...menuItemProps}>
74106
<Box padding={2}>
75107
<Typography>{children}</Typography>
@@ -80,14 +112,16 @@ export const MenuItem = ({ children, onClick, to, isFocused, ...props }) => {
80112

81113
MenuItem.defaultProps = {
82114
as: undefined,
83-
onClick() {},
115+
href: undefined,
84116
isFocused: false,
117+
onClick() {},
85118
to: undefined,
86119
};
87120

88121
MenuItem.propTypes = {
89122
as: PropTypes.elementType,
90123
children: PropTypes.node.isRequired,
124+
href: PropTypes.string,
91125
isFocused: PropTypes.bool,
92126
onClick: PropTypes.func,
93127
to: PropTypes.string,

packages/strapi-design-system/src/SimpleMenu/SimpleMenu.stories.mdx

+15-34
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,10 @@ return (
7878

7979
### With links
8080

81+
To use `MenuItem` as an internal link you can use the `to` prop,
82+
it will render a `NavLink` react-router-dom component.
83+
If you need to use a different routing library, it is advised to use `v2/SimpleMenu` component [found here](http://localhost:6006/?path=/docs/design-system-components-v2-simplemenu--base#with-other-routing-libraries).
84+
8185
<Canvas>
8286
<Story
8387
name="with-links"
@@ -92,6 +96,9 @@ return (
9296
<MenuItem to="/somewhere">
9397
Somewhere internal
9498
</MenuItem>
99+
<MenuItem href="https://strapi.io/" isExternal>
100+
Somewhere External
101+
</MenuItem>
95102
</SimpleMenu>
96103
`,
97104
},
@@ -101,19 +108,25 @@ return (
101108
<SimpleMenu label="Menu">
102109
<MenuItem to="/">Home</MenuItem>
103110
<MenuItem to="/somewhere">Somewhere internal</MenuItem>
111+
<MenuItem href="https://strapi.io/" isExternal>
112+
Somewhere External
113+
</MenuItem>
104114
</SimpleMenu>
105115
</Story>
106116
</Canvas>
107117

108-
A menu can allow the user to make actions. It can redirect the user to another page or another part of the product.
109-
110118
### With IconButton
111119

120+
A menu can allow the user to make actions. It can redirect the user to another page or another part of the product.
121+
112122
<Canvas>
113123
<Story name="with-iconbutton">
114124
<SimpleMenu label="Menu" as={IconButton} icon={<CarretDown />}>
115125
<MenuItem>Home</MenuItem>
116126
<MenuItem>Somewhere internal</MenuItem>
127+
<MenuItem href="https://strapi.io/" isExternal>
128+
Somewhere External
129+
</MenuItem>
117130
</SimpleMenu>
118131
</Story>
119132
</Canvas>
@@ -179,38 +192,6 @@ return (
179192
</Story>
180193
</Canvas>
181194

182-
### with other routing libraries
183-
184-
To use the SimpleMenu/MenuItem components with a routing library (e.g. react-router-dom), you'll need to pass the
185-
`NavLink` component to the `as` prop in order to replace the default HTML anchor `<a />`. You'll also need to pass the
186-
`isLink` props to the `MenuItem` component.
187-
188-
```jsx
189-
import { SimpleMenu, MenuItem } from '@strapi/design-system';
190-
import { NavLink as RouterNavLink } from 'react-router-dom';
191-
192-
<SimpleMenu label={Label}>
193-
<MenuItem as={RouterNavLink} to="/">
194-
Home
195-
</MenuItem>
196-
</SimpleMenu>;
197-
```
198-
199-
#### with NextJS
200-
201-
For NextJS, you'll need to wrap the `NavLink` with the `NextLink` component
202-
203-
```jsx
204-
import { SimpleMenu, MenuItem } from '@strapi/design-system';
205-
import NextLink from 'next/link';
206-
207-
<SimpleMenu label={Label}>
208-
<NextLink href="/home" passHref>
209-
<MenuItem>Home</MenuItem>
210-
</NextLink>
211-
</SimpleMenu>;
212-
```
213-
214195
## Accessibility
215196

216197
- When the menu button has focus, `Space` or `Enter` opens or closes the menu.

packages/strapi-design-system/src/SimpleMenu/__tests__/SimpleMenu.spec.js

+31-6
Original file line numberDiff line numberDiff line change
@@ -8,15 +8,17 @@ describe('SimpleMenu', () => {
88
it('display the menu on click on the menu button', async () => {
99
render(
1010
<ThemeProvider theme={lightTheme}>
11-
<SimpleMenu label="January">
11+
<SimpleMenu label="Menu">
1212
<MenuItem onClick={() => {}}>January</MenuItem>
1313
<MenuItem onClick={() => {}}>February</MenuItem>
14-
<MenuItem href="https://strapi.io">Strapi website</MenuItem>
14+
<MenuItem href="https://strapi.io" isExternal>
15+
Strapi website
16+
</MenuItem>
1517
</SimpleMenu>
1618
</ThemeProvider>,
1719
);
1820

19-
const button = await waitFor(() => screen.getByText('January'));
21+
const button = await waitFor(() => screen.getByText('Menu'));
2022
fireEvent.mouseDown(button);
2123

2224
await waitFor(() => {
@@ -29,20 +31,43 @@ describe('SimpleMenu', () => {
2931

3032
render(
3133
<ThemeProvider theme={lightTheme}>
32-
<SimpleMenu label="January">
34+
<SimpleMenu label="Menu">
3335
<MenuItem onClick={onClickSpy}>January</MenuItem>
3436
<MenuItem onClick={onClickSpy}>February</MenuItem>
35-
<MenuItem href="https://strapi.io">Strapi website</MenuItem>
37+
<MenuItem href="https://strapi.io" isExternal>
38+
Strapi website
39+
</MenuItem>
3640
</SimpleMenu>
3741
</ThemeProvider>,
3842
);
3943

40-
const button = await waitFor(() => screen.getByText('January'));
44+
const button = await waitFor(() => screen.getByText('Menu'));
4145
fireEvent.mouseDown(button);
4246

4347
const menuItemButton = await waitFor(() => screen.getByText('February'));
4448
fireEvent.mouseDown(menuItemButton);
4549

4650
expect(onClickSpy).toBeCalled();
4751
});
52+
53+
it('display the menu on click on the external link menu button', async () => {
54+
render(
55+
<ThemeProvider theme={lightTheme}>
56+
<SimpleMenu label="Menu">
57+
<MenuItem onClick={() => {}}>January</MenuItem>
58+
<MenuItem onClick={() => {}}>February</MenuItem>
59+
<MenuItem href="https://strapi.io" isExternal>
60+
Strapi website
61+
</MenuItem>
62+
</SimpleMenu>
63+
</ThemeProvider>,
64+
);
65+
66+
const button = await waitFor(() => screen.getByText('Menu'));
67+
fireEvent.mouseDown(button);
68+
69+
await waitFor(() => {
70+
expect(screen.getByText('Strapi website').closest('a')).toHaveAttribute('href', 'https://strapi.io');
71+
});
72+
});
4873
});

packages/strapi-design-system/src/v2/SimpleMenu/SimpleMenu.js

+41-8
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { useCallbackRef } from '@radix-ui/react-use-callback-ref';
55

66
import CarretDown from '@strapi/icons/CarretDown';
77

8+
import { Link } from '../Link';
89
import { Typography } from '../../Typography';
910
import { Box } from '../../Box';
1011
import { Flex } from '../../Flex';
@@ -28,6 +29,22 @@ const OptionLink = styled(BaseLink)`
2829
${getOptionStyle}
2930
`;
3031

32+
const OptionExternalLink = styled(Link)`
33+
&:focus-visible {
34+
/* Removes Link focus-visible after properties and reset to global outline */
35+
outline: 2px solid ${({ theme }) => theme.colors.primary600};
36+
outline-offset: 2px;
37+
&:after {
38+
content: none;
39+
}
40+
}
41+
/* Removes Link svg color */
42+
svg path {
43+
fill: currentColor;
44+
}
45+
${getOptionStyle}
46+
`;
47+
3148
const IconWrapper = styled.span`
3249
display: flex;
3350
align-items: center;
@@ -41,7 +58,7 @@ const StyledButtonSmall = styled(Button)`
4158
padding: ${({ theme }) => `${theme.spaces[1]} ${theme.spaces[3]}`};
4259
`;
4360

44-
export const MenuItem = ({ as, children, onClick, isFocused, isLink, ...props }) => {
61+
export const MenuItem = ({ as, children, onClick, isFocused, isLink, isExternal, ...props }) => {
4562
const menuItemRef = useRef();
4663

4764
useEffect(() => {
@@ -63,13 +80,27 @@ export const MenuItem = ({ as, children, onClick, isFocused, isLink, ...props })
6380
}
6481
};
6582

66-
return isLink ? (
67-
<OptionLink as={as} {...menuItemProps}>
68-
<Box padding={2}>
69-
<Typography>{children}</Typography>
70-
</Box>
71-
</OptionLink>
72-
) : (
83+
if (isLink) {
84+
return (
85+
<OptionLink as={as} {...menuItemProps}>
86+
<Box padding={2}>
87+
<Typography>{children}</Typography>
88+
</Box>
89+
</OptionLink>
90+
);
91+
}
92+
93+
if (isExternal) {
94+
return (
95+
<OptionExternalLink isExternal {...menuItemProps}>
96+
<Box padding={2}>
97+
<Typography>{children}</Typography>
98+
</Box>
99+
</OptionExternalLink>
100+
);
101+
}
102+
103+
return (
73104
<OptionButton onKeyDown={handleKeyDown} onMouseDown={onClick} type="button" {...menuItemProps}>
74105
<Box padding={2}>
75106
<Typography>{children}</Typography>
@@ -81,13 +112,15 @@ export const MenuItem = ({ as, children, onClick, isFocused, isLink, ...props })
81112
MenuItem.defaultProps = {
82113
as: undefined,
83114
onClick() {},
115+
isExternal: false,
84116
isFocused: false,
85117
isLink: false,
86118
};
87119

88120
MenuItem.propTypes = {
89121
as: PropTypes.elementType,
90122
children: PropTypes.node.isRequired,
123+
isExternal: PropTypes.bool,
91124
isFocused: PropTypes.bool,
92125
isLink: PropTypes.bool,
93126
onClick: PropTypes.func,

0 commit comments

Comments
 (0)