Skip to content

Commit 49eafd7

Browse files
committed
Merge branch 'develop' into feat/pagination-copy
2 parents 2fb4389 + 06e4732 commit 49eafd7

24 files changed

Lines changed: 345 additions & 458 deletions

File tree

client/common/Tooltip.test.tsx

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
import React from 'react';
2+
import userEvent from '@testing-library/user-event';
3+
import { render, screen } from '../test-utils';
4+
import { Tooltip } from './Tooltip';
5+
6+
describe('Tooltip', () => {
7+
it('renders the child element', () => {
8+
render(
9+
<Tooltip content="This is a tooltip">
10+
<button>Hover me</button>
11+
</Tooltip>
12+
);
13+
expect(screen.getByRole('button')).toBeInTheDocument();
14+
expect(screen.getByText('Hover me')).toBeInTheDocument();
15+
});
16+
17+
it('does not show the tooltip when the user is not hovering over the element', () => {
18+
render(
19+
<Tooltip content="Tooltip text">
20+
<button>Button</button>
21+
</Tooltip>
22+
);
23+
24+
const button = screen.getByRole('button');
25+
expect(button).toBeInTheDocument();
26+
expect(button).not.toHaveClass('tooltipped-visible');
27+
});
28+
29+
it('shows the tooltip if the user hovers over the element', async () => {
30+
const user = userEvent.setup();
31+
render(
32+
<Tooltip content="Tooltip text">
33+
<button>Button</button>
34+
</Tooltip>
35+
);
36+
37+
const button = screen.getByRole('button');
38+
await user.hover(button);
39+
40+
expect(button).toHaveClass('tooltipped');
41+
expect(button).toHaveAttribute('aria-label', 'Tooltip text');
42+
});
43+
44+
it('adds the aria-label with tooltip content to the child element', () => {
45+
render(
46+
<Tooltip content="Save your changes">
47+
<button>Save</button>
48+
</Tooltip>
49+
);
50+
51+
const button = screen.getByRole('button');
52+
expect(button).toHaveAttribute('aria-label', 'Save your changes');
53+
});
54+
55+
it('applies tooltipped-no-delay class when noDelay is true', () => {
56+
render(
57+
<Tooltip content="No delay tooltip" noDelay>
58+
<button>Button</button>
59+
</Tooltip>
60+
);
61+
62+
const button = screen.getByRole('button');
63+
expect(button).toHaveClass('tooltipped-no-delay');
64+
});
65+
66+
it('does not apply tooltipped-no-delay class when noDelay is false', () => {
67+
render(
68+
<Tooltip content="Normal tooltip" noDelay={false}>
69+
<button>Button</button>
70+
</Tooltip>
71+
);
72+
73+
const button = screen.getByRole('button');
74+
expect(button).not.toHaveClass('tooltipped-no-delay');
75+
});
76+
77+
it('preserves existing className on the child element', () => {
78+
render(
79+
<Tooltip content="Tooltip">
80+
<button className="custom-class">Button</button>
81+
</Tooltip>
82+
);
83+
84+
const button = screen.getByRole('button');
85+
expect(button).toHaveClass('custom-class');
86+
expect(button).toHaveClass('tooltipped');
87+
});
88+
89+
it('wraps the child in a tooltip-wrapper span', () => {
90+
const { container } = render(
91+
<Tooltip content="Tooltip">
92+
<button>Button</button>
93+
</Tooltip>
94+
);
95+
96+
const wrapper = container.querySelector('.tooltip-wrapper');
97+
expect(wrapper).toBeInTheDocument();
98+
expect(wrapper?.tagName.toLowerCase()).toBe('span');
99+
});
100+
});

client/common/Tooltip.tsx

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import React, { ReactElement, useMemo } from 'react';
2+
3+
export type TooltipProps = {
4+
content: string;
5+
noDelay?: boolean;
6+
children: ReactElement;
7+
};
8+
9+
export function Tooltip({ content, noDelay = false, children }: TooltipProps) {
10+
const tooltipClasses = useMemo(() => {
11+
const existingClassName = children.props?.className || '';
12+
return [
13+
existingClassName,
14+
'tooltipped',
15+
'tooltipped-n',
16+
noDelay && 'tooltipped-no-delay'
17+
]
18+
.filter(Boolean)
19+
.join(' ');
20+
}, [children.props?.className, noDelay]);
21+
22+
const childProps = useMemo(
23+
() => ({
24+
'aria-label': content,
25+
className: tooltipClasses
26+
}),
27+
[content, tooltipClasses]
28+
);
29+
30+
return (
31+
<span className="tooltip-wrapper">
32+
{React.cloneElement(children, childProps)}
33+
</span>
34+
);
35+
}

client/components/Menubar/MenubarItem.tsx

Lines changed: 26 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import React, { useEffect, useContext, useRef } from 'react';
22
import { MenubarContext, SubmenuContext, ParentMenuContext } from './contexts';
33
import { ButtonOrLink, ButtonOrLinkProps } from '../../common/ButtonOrLink';
4+
import { Tooltip, TooltipProps } from '../../common/Tooltip';
45

56
export enum MenubarItemRole {
67
MENU_ITEM = 'menuitem',
@@ -13,6 +14,7 @@ export interface MenubarItemProps extends Omit<ButtonOrLinkProps, 'role'> {
1314
*/
1415
role?: MenubarItemRole;
1516
selected?: boolean;
17+
tooltipContent?: TooltipProps['content'];
1618
}
1719

1820
/**
@@ -54,6 +56,7 @@ export function MenubarItem({
5456
role: customRole = MenubarItemRole.MENU_ITEM,
5557
isDisabled = false,
5658
selected = false,
59+
tooltipContent,
5760
...rest
5861
}: MenubarItemProps) {
5962
const { createMenuItemHandlers, hasFocus } = useContext(MenubarContext);
@@ -94,15 +97,29 @@ export function MenubarItem({
9497
ref={menuItemRef}
9598
onMouseEnter={handleMouseEnter}
9699
>
97-
<ButtonOrLink
98-
{...rest}
99-
{...handlers}
100-
{...ariaSelected}
101-
role={role}
102-
tabIndex={-1}
103-
id={id}
104-
isDisabled={isDisabled}
105-
/>
100+
{tooltipContent ? (
101+
<Tooltip content={tooltipContent}>
102+
<ButtonOrLink
103+
{...rest}
104+
{...handlers}
105+
{...ariaSelected}
106+
role={role}
107+
tabIndex={-1}
108+
id={id}
109+
isDisabled={isDisabled}
110+
/>
111+
</Tooltip>
112+
) : (
113+
<ButtonOrLink
114+
{...rest}
115+
{...handlers}
116+
{...ariaSelected}
117+
role={role}
118+
tabIndex={-1}
119+
id={id}
120+
isDisabled={isDisabled}
121+
/>
122+
)}
106123
</li>
107124
);
108125
}

client/modules/About/pages/About.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -167,7 +167,7 @@ export const About = () => {
167167
<a
168168
href="https://github.com/processing/p5.js-web-editor/releases"
169169
target="_blank"
170-
rel="noreferrer"
170+
rel="noopener noreferrer"
171171
>
172172
{t('About.WebEditor')}: <span>v{packageData?.version}</span>
173173
</a>
@@ -176,7 +176,7 @@ export const About = () => {
176176
<a
177177
href="https://github.com/processing/p5.js/releases"
178178
target="_blank"
179-
rel="noreferrer"
179+
rel="noopener noreferrer"
180180
>
181181
p5.js: <span>v{p5version}</span>
182182
</a>

client/modules/IDE/actions/assets.js

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,10 @@ export function getAssets() {
3030
export function deleteAssetRequest(assetKey) {
3131
return async (dispatch) => {
3232
try {
33-
await apiClient.delete(`/S3/${assetKey}`);
33+
const path = assetKey.split('/').pop();
34+
await apiClient.delete(
35+
`/S3/delete?objectKey=${encodeURIComponent(path)}`
36+
);
3437
dispatch(deleteAsset(assetKey));
3538
} catch (error) {
3639
dispatch({

client/modules/IDE/components/AssetListRow.jsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ const AssetMenu = ({ item: asset }) => {
2222
return (
2323
<TableDropdown aria-label={t('AssetList.ToggleOpenCloseARIA')}>
2424
<MenuItem onClick={handleAssetDelete}>{t('AssetList.Delete')}</MenuItem>
25-
<MenuItem href={asset.url} target="_blank">
25+
<MenuItem href={asset.url} target="_blank" rel="noopener noreferrer">
2626
{t('AssetList.OpenNewTab')}
2727
</MenuItem>
2828
</TableDropdown>

client/modules/IDE/components/Header/Nav.jsx

Lines changed: 29 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -185,7 +185,12 @@ const ProjectMenu = () => {
185185
isDisabled={
186186
!user.authenticated ||
187187
!isLoginEnabled ||
188-
(project?.owner && !isUserOwner)
188+
(!!project?.owner && !isUserOwner)
189+
}
190+
tooltipContent={
191+
!user.authenticated || !isLoginEnabled
192+
? t('Nav.File.SaveTooltipUnauthenticated')
193+
: undefined
189194
}
190195
onClick={() => saveSketch(cmRef.current)}
191196
>
@@ -194,36 +199,57 @@ const ProjectMenu = () => {
194199
</MenubarItem>
195200
<MenubarItem
196201
id="file-duplicate"
197-
isDisabled={isUnsaved || !user.authenticated}
202+
isDisabled={!user.authenticated || isUnsaved}
203+
tooltipContent={
204+
!user.authenticated
205+
? t('Nav.File.DuplicateTooltipUnauthenticated')
206+
: undefined
207+
}
198208
onClick={() => dispatch(cloneProject())}
199209
>
200210
{t('Nav.File.Duplicate')}
201211
</MenubarItem>
202212
<MenubarItem
203213
id="file-share"
204214
isDisabled={isUnsaved}
215+
tooltipContent={
216+
isUnsaved ? t('Nav.File.ShareTooltipUnsaved') : undefined
217+
}
205218
onClick={shareSketch}
206219
>
207220
{t('Nav.File.Share')}
208221
</MenubarItem>
209222
<MenubarItem
210223
id="file-download"
211224
isDisabled={isUnsaved}
225+
tooltipContent={
226+
isUnsaved ? t('Nav.File.DownloadTooltipUnsaved') : undefined
227+
}
212228
onClick={downloadSketch}
213229
>
214230
{t('Nav.File.Download')}
215231
</MenubarItem>
216232
<MenubarItem
217233
id="file-open"
218234
isDisabled={!user.authenticated}
235+
tooltipContent={
236+
!user.authenticated
237+
? t('Nav.File.OpenTooltipUnauthenticated')
238+
: undefined
239+
}
219240
href={`/${user.username}/sketches`}
220241
>
221242
{t('Nav.File.Open')}
222243
</MenubarItem>
223244
<MenubarItem
224245
id="file-add-to-collection"
225246
isDisabled={
226-
!isUiCollectionsEnabled || !user.authenticated || isUnsaved
247+
!user.authenticated || !isUiCollectionsEnabled || isUnsaved
248+
}
249+
tooltipContent={
250+
!user.authenticated
251+
? t('Nav.File.AddToCollectionTooltipUnauthenticated')
252+
: undefined
227253
}
228254
href={`/${user.username}/sketches/${project?.id}/add-to-collection`}
229255
>

client/modules/IDE/components/Preferences/index.jsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -118,7 +118,7 @@ export default function Preferences() {
118118
const markdownComponents = useMemo(() => {
119119
// eslint-disable-next-line react/no-unstable-nested-components
120120
const ExternalLink = ({ children, ...props }) => (
121-
<a {...props} target="_blank">
121+
<a {...props} target="_blank" rel="noopener noreferrer">
122122
{children}
123123
</a>
124124
);
@@ -618,7 +618,7 @@ export default function Preferences() {
618618
<legend className="preference__warning">
619619
<a
620620
target="_blank"
621-
rel="noreferrer"
621+
rel="noopener noreferrer"
622622
href={`https://${
623623
versionInfo.isVersion2 ? 'beta.' : ''
624624
}p5js.org/reference/p5.sound`}

client/modules/IDE/components/QuickAddList/QuickAddList.jsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ const Item = ({ isAdded, onSelect, name, url }) => {
3737
className="quick-add__item-view"
3838
to={url}
3939
target="_blank"
40+
rel="noopener noreferrer"
4041
onClick={(e) => e.stopPropagation()}
4142
>
4243
{t('QuickAddList.View')}

client/modules/IDE/components/Sidebar.jsx

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import { getAuthenticated, selectCanEditSketch } from '../selectors/users';
1616
import ConnectedFileNode from './FileNode';
1717
import { PlusIcon } from '../../../common/icons';
1818
import { FileDrawer } from './Editor/MobileEditor';
19+
import { Tooltip } from '../../../common/Tooltip';
1920

2021
// TODO: use a generic Dropdown UI component
2122

@@ -124,8 +125,8 @@ export default function SideBar() {
124125
{t('Sidebar.AddFile')}
125126
</button>
126127
</li>
127-
{isAuthenticated && (
128-
<li>
128+
<li>
129+
{isAuthenticated ? (
129130
<button
130131
aria-label={t('Sidebar.UploadFileARIA')}
131132
onClick={() => {
@@ -135,8 +136,24 @@ export default function SideBar() {
135136
>
136137
{t('Sidebar.UploadFile')}
137138
</button>
138-
</li>
139-
)}
139+
) : (
140+
<Tooltip
141+
content={t('Sidebar.UploadFileTooltipUnauthenticated')}
142+
>
143+
<button
144+
aria-label={t('Sidebar.UploadFileARIA')}
145+
aria-disabled
146+
onClick={(e) => {
147+
// prevent any action when unauthenticated
148+
e.preventDefault();
149+
e.stopPropagation();
150+
}}
151+
>
152+
{t('Sidebar.UploadFile')}
153+
</button>
154+
</Tooltip>
155+
)}
156+
</li>
140157
</ul>
141158
)}
142159
</div>

0 commit comments

Comments
 (0)