Skip to content

Commit c9a9e3b

Browse files
authored
Merge branch 'develop' into fix/hindi-translation
2 parents 056fd46 + cd7052b commit c9a9e3b

11 files changed

Lines changed: 283 additions & 22 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/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/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>

client/styles/abstracts/_placeholders.scss

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -230,7 +230,7 @@
230230
background-color: getThemifyVariable('button-background-hover-color');
231231
color: getThemifyVariable('button-hover-color')
232232
}
233-
& button, & a {
233+
& button, & a, & .tooltip-wrapper button {
234234
@include themify() {
235235
color: getThemifyVariable('button-hover-color');
236236
}

client/styles/components/_nav.scss

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -174,8 +174,8 @@
174174
.nav__dropdown {
175175
@extend %dropdown-open-left;
176176
display: none;
177-
max-height: 60vh;
178-
overflow-y: auto;
177+
max-height: none;
178+
overflow: visible;
179179
.nav__item--open & {
180180
display: flex;
181181
}
@@ -212,13 +212,24 @@
212212

213213
.nav__dropdown-item {
214214
& button,
215-
& a {
215+
& a,
216+
& .tooltip-wrapper button,
217+
& .tooltip-wrapper a {
216218
width: 100%;
217219
height: 100%;
218220
display: flex;
219221
justify-content: space-between;
220222
align-items: center;
221223
}
224+
225+
&:hover {
226+
& .tooltip-wrapper button,
227+
& .tooltip-wrapper a {
228+
@include themify() {
229+
color: getThemifyVariable('button-hover-color');
230+
}
231+
}
232+
}
222233
}
223234

224235
.nav__item-logo {
@@ -253,6 +264,7 @@
253264
.nav__keyboard-shortcut {
254265
font-size: #{math.div(12, $base-font-size)}rem;
255266
font-family: Inconsololata, monospace;
267+
margin-left: auto;
256268

257269
@include themify() {
258270
color: getThemifyVariable('keyboard-shortcut-color');

0 commit comments

Comments
 (0)