Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
79 changes: 79 additions & 0 deletions client/common/Tooltip.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import React, { ReactElement, useRef, useState } from 'react';

export type TooltipDirection = 'n' | 's' | 'e' | 'w';

export type TooltipProps = {
content: string;
direction?: TooltipDirection;
noDelay?: boolean;
children: ReactElement;
};

export function Tooltip({
content,
direction = 'n',
noDelay = false,
children
}: TooltipProps) {
const [open, setOpen] = useState(false);
const tooltipIdRef = useRef(`tooltip-${Math.random().toString(36).slice(2)}`);

const childProps: Record<string, unknown> = {
'aria-label': content,
className: [
(children.props && children.props.className) || '',
'tooltipped',
`tooltipped-${direction}`,
noDelay ? 'tooltipped-no-delay' : ''
]
.filter(Boolean)
.join(' '),
'aria-describedby': tooltipIdRef.current,
onFocus: (e: React.FocusEvent) => {
setOpen(true);
if (children.props && typeof children.props.onFocus === 'function') {
children.props.onFocus(e);
}
},
onBlur: (e: React.FocusEvent) => {
setOpen(false);
if (children.props && typeof children.props.onBlur === 'function') {
children.props.onBlur(e);
}
},
onMouseEnter: (e: React.MouseEvent) => {
setOpen(true);
if (children.props && typeof children.props.onMouseEnter === 'function') {
children.props.onMouseEnter(e);
}
},
onMouseLeave: (e: React.MouseEvent) => {
setOpen(false);
if (children.props && typeof children.props.onMouseLeave === 'function') {
children.props.onMouseLeave(e);
}
},
onKeyDown: (e: React.KeyboardEvent) => {
if (e.key === 'Escape') {
setOpen(false);
(e.target as HTMLElement)?.blur();
}
if (children.props && typeof children.props.onKeyDown === 'function') {
children.props.onKeyDown(e);
}
}
};

const trigger = React.cloneElement(children, childProps);

return (
<span className="tooltip-wrapper">
{trigger}
{open && (
<span id={tooltipIdRef.current} role="tooltip" className="sr-only">
{content}
</span>
)}
</span>
);
}
35 changes: 26 additions & 9 deletions client/components/Menubar/MenubarItem.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import React, { useEffect, useContext, useRef } from 'react';
import { MenubarContext, SubmenuContext, ParentMenuContext } from './contexts';
import { ButtonOrLink, ButtonOrLinkProps } from '../../common/ButtonOrLink';
import { Tooltip, TooltipDirection } from '../../common/Tooltip';

export enum MenubarItemRole {
MENU_ITEM = 'menuitem',
Expand All @@ -13,6 +14,8 @@ export interface MenubarItemProps extends Omit<ButtonOrLinkProps, 'role'> {
*/
role?: MenubarItemRole;
selected?: boolean;
tooltipContent?: string;
tooltipDirection?: TooltipDirection;
}

/**
Expand Down Expand Up @@ -54,6 +57,8 @@ export function MenubarItem({
role: customRole = MenubarItemRole.MENU_ITEM,
isDisabled = false,
selected = false,
tooltipContent,
tooltipDirection,
...rest
}: MenubarItemProps) {
const { createMenuItemHandlers, hasFocus } = useContext(MenubarContext);
Expand Down Expand Up @@ -86,6 +91,26 @@ export function MenubarItem({
return unregister;
}, [submenuItems, registerSubmenuItem]);

const buttonOrLink = (
<ButtonOrLink
{...rest}
{...handlers}
{...ariaSelected}
role={role}
tabIndex={-1}
id={id}
isDisabled={isDisabled}
/>
);

const content = tooltipContent ? (
<Tooltip content={tooltipContent} direction={tooltipDirection}>
{buttonOrLink}
</Tooltip>
) : (
buttonOrLink
);

return (
<li
className={`${className} ${
Expand All @@ -94,15 +119,7 @@ export function MenubarItem({
ref={menuItemRef}
onMouseEnter={handleMouseEnter}
>
<ButtonOrLink
{...rest}
{...handlers}
{...ariaSelected}
role={role}
tabIndex={-1}
id={id}
isDisabled={isDisabled}
/>
{content}
</li>
);
}
32 changes: 29 additions & 3 deletions client/modules/IDE/components/Header/Nav.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -185,7 +185,12 @@ const ProjectMenu = () => {
isDisabled={
!user.authenticated ||
!isLoginEnabled ||
(project?.owner && !isUserOwner)
(!!project?.owner && !isUserOwner)
}
tooltipContent={
!user.authenticated || !isLoginEnabled
? t('Nav.File.SaveTooltipUnauthenticated')
: undefined
}
onClick={() => saveSketch(cmRef.current)}
>
Expand All @@ -194,36 +199,57 @@ const ProjectMenu = () => {
</MenubarItem>
<MenubarItem
id="file-duplicate"
isDisabled={isUnsaved || !user.authenticated}
isDisabled={!user.authenticated || isUnsaved}
tooltipContent={
!user.authenticated
? t('Nav.File.DuplicateTooltipUnauthenticated')
: undefined
}
onClick={() => dispatch(cloneProject())}
>
{t('Nav.File.Duplicate')}
</MenubarItem>
<MenubarItem
id="file-share"
isDisabled={isUnsaved}
tooltipContent={
isUnsaved ? t('Nav.File.ShareTooltipUnsaved') : undefined
}
onClick={shareSketch}
>
{t('Nav.File.Share')}
</MenubarItem>
<MenubarItem
id="file-download"
isDisabled={isUnsaved}
tooltipContent={
isUnsaved ? t('Nav.File.DownloadTooltipUnsaved') : undefined
}
onClick={downloadSketch}
>
{t('Nav.File.Download')}
</MenubarItem>
<MenubarItem
id="file-open"
isDisabled={!user.authenticated}
tooltipContent={
!user.authenticated
? t('Nav.File.OpenTooltipUnauthenticated')
: undefined
}
href={`/${user.username}/sketches`}
>
{t('Nav.File.Open')}
</MenubarItem>
<MenubarItem
id="file-add-to-collection"
isDisabled={
!isUiCollectionsEnabled || !user.authenticated || isUnsaved
!user.authenticated || !isUiCollectionsEnabled || isUnsaved
}
tooltipContent={
!user.authenticated
? t('Nav.File.AddToCollectionTooltipUnauthenticated')
: undefined
}
href={`/${user.username}/sketches/${project?.id}/add-to-collection`}
>
Expand Down
26 changes: 22 additions & 4 deletions client/modules/IDE/components/Sidebar.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import { getAuthenticated, selectCanEditSketch } from '../selectors/users';
import ConnectedFileNode from './FileNode';
import { PlusIcon } from '../../../common/icons';
import { FileDrawer } from './Editor/MobileEditor';
import { Tooltip } from '../../../common/Tooltip';

// TODO: use a generic Dropdown UI component

Expand Down Expand Up @@ -124,8 +125,8 @@ export default function SideBar() {
{t('Sidebar.AddFile')}
</button>
</li>
{isAuthenticated && (
<li>
<li>
{isAuthenticated ? (
<button
aria-label={t('Sidebar.UploadFileARIA')}
onClick={() => {
Expand All @@ -135,8 +136,25 @@ export default function SideBar() {
>
{t('Sidebar.UploadFile')}
</button>
</li>
)}
) : (
<Tooltip
content={t('Sidebar.UploadFileTooltipUnauthenticated')}
direction="n"
>
<button
aria-label={t('Sidebar.UploadFileARIA')}
aria-disabled
onClick={(e) => {
// prevent any action when unauthenticated
e.preventDefault();
e.stopPropagation();
}}
>
{t('Sidebar.UploadFile')}
</button>
</Tooltip>
)}
</li>
</ul>
)}
</div>
Expand Down
5 changes: 3 additions & 2 deletions client/styles/components/_nav.scss
Original file line number Diff line number Diff line change
Expand Up @@ -174,8 +174,8 @@
.nav__dropdown {
@extend %dropdown-open-left;
display: none;
max-height: 60vh;
overflow-y: auto;
max-height: none;
overflow: visible;
.nav__item--open & {
display: flex;
}
Expand Down Expand Up @@ -253,6 +253,7 @@
.nav__keyboard-shortcut {
font-size: #{math.div(12, $base-font-size)}rem;
font-family: Inconsololata, monospace;
margin-left: auto;

@include themify() {
color: getThemifyVariable('keyboard-shortcut-color');
Expand Down
31 changes: 31 additions & 0 deletions client/styles/components/_tooltip.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
.tooltip-wrapper {
position: relative;
display: inline-flex;
}

.tooltip-wrapper .tooltipped::after {
@include themify() {
background-color: getThemifyVariable('button-background-hover-color');
color: getThemifyVariable('button-hover-color');
}
font-family: Montserrat, sans-serif;
font-size: 1rem;
padding: 0.5rem 0.75rem;
max-width: none;
white-space: nowrap;
left: 1rem;
right: auto;
transform: translateX(0);
text-align: left;
}

.tooltip-wrapper .tooltipped-n::before,
.tooltip-wrapper .tooltipped::before {
@include themify() {
color: getThemifyVariable('button-background-hover-color');
border-top-color: getThemifyVariable('button-background-hover-color');
}
left: 1.75rem;
right: auto;
transform: translateX(0);
}
1 change: 1 addition & 0 deletions client/styles/main.scss
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@
@import 'components/admonition';
@import 'components/banner';
@import 'components/visibility-dropdown';
@import 'components/tooltip';

@import 'layout/dashboard';
@import 'layout/ide';
11 changes: 9 additions & 2 deletions translations/locales/en-US/translations.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,13 @@
"Open": "Open",
"Download": "Download",
"AddToCollection": "Add to Collection",
"Examples": "Examples"
"Examples": "Examples",
"SaveTooltipUnauthenticated": "Log in to save your sketch",
"DuplicateTooltipUnauthenticated": "Log in to duplicate this sketch",
"OpenTooltipUnauthenticated": "Log in to open your sketches",
"AddToCollectionTooltipUnauthenticated": "Log in to add to collections",
"ShareTooltipUnsaved": "Save your sketch before sharing",
"DownloadTooltipUnsaved": "Save your sketch before downloading"
},
"Edit": {
"Title": "Edit",
Expand Down Expand Up @@ -275,7 +281,8 @@
"AddFile": "Create file",
"AddFileARIA": "add file",
"UploadFile": "Upload file",
"UploadFileARIA": "upload file"
"UploadFileARIA": "upload file",
"UploadFileTooltipUnauthenticated": "Log in to upload files"
},
"FileNode": {
"OpenFolderARIA": "Open folder contents",
Expand Down
Loading