Add tooltip for tabs (#7482)

This commit is contained in:
AlexandreS
2025-03-20 10:27:38 -07:00
committed by GitHub
parent e432a7fa67
commit 495b99356d
7 changed files with 266 additions and 22 deletions

View File

@@ -330,7 +330,7 @@ const CourseSection = ({
style={styles.desktopTableOfContent}
>
<Text noMargin size="sub-title">
Chapters
<Trans>Chapters</Trans>
</Text>
{courseCompletion !== null && (
<Line noMargin>

View File

@@ -33,9 +33,10 @@ type DraggableEditorTabsProps = {|
onCloseAll: () => void,
onTabActivated: (editor: EditorTab) => void,
onDropTab: (fromIndex: number, toHoveredIndex: number) => void,
onHoverTab: (editor: ?EditorTab) => void,
|};
const getTabId = (editorTab: EditorTab) =>
export const getTabId = (editorTab: EditorTab) =>
`tab-${editorTab.key.replace(/\s/g, '-')}`;
const homeTabApproximateWidth = 35;
@@ -49,6 +50,7 @@ export function DraggableEditorTabs({
onCloseAll,
onTabActivated,
onDropTab,
onHoverTab,
}: DraggableEditorTabsProps) {
let draggedTabIndex: ?number = null;
@@ -108,6 +110,7 @@ export function DraggableEditorTabs({
onClose={() => onCloseTab(editorTab)}
onCloseOthers={() => onCloseOtherTabs(editorTab)}
onCloseAll={onCloseAll}
onHover={(enter: boolean) => onHoverTab(enter ? editorTab : null)}
onActivated={() => onTabActivated(editorTab)}
closable={editorTab.closable}
onBeginDrag={() => {
@@ -152,6 +155,7 @@ export function DraggableClosableTab({
onActivated,
onBeginDrag,
onDrop,
onHover,
maxWidth,
}: DraggableClosableTabProps) {
return (
@@ -190,6 +194,7 @@ export function DraggableClosableTab({
renderCustomIcon={renderCustomIcon}
closable={closable}
onClick={onClick}
onHover={onHover}
onActivated={onActivated}
maxWidth={maxWidth}
key={id}

View File

@@ -1,5 +1,6 @@
// @flow
import * as React from 'react';
import MenuIcon from '../UI/CustomSvgIcons/Menu';
import IconButton from '../UI/IconButton';
import GDevelopThemeContext from '../UI/Theme/GDevelopThemeContext';
@@ -8,6 +9,10 @@ import {
TitleBarLeftSafeMargins,
TitleBarRightSafeMargins,
} from '../UI/TitleBarSafeMargins';
import { type EditorTab } from './EditorTabs/EditorTabsHandler';
import { getTabId } from './EditorTabs/DraggableEditorTabs';
import { useScreenType } from '../UI/Responsive/ScreenTypeMeasurer';
import TabsTitlebarTooltip from './TabsTitlebarTooltip';
const WINDOW_DRAGGABLE_PART_CLASS_NAME = 'title-bar-draggable-part';
const WINDOW_NON_DRAGGABLE_PART_CLASS_NAME = 'title-bar-non-draggable-part';
@@ -30,21 +35,27 @@ const styles = {
};
type TabsTitlebarProps = {|
children: React.Node,
hidden: boolean,
toggleProjectManager: () => void,
renderTabs: (onHoverEditorTab: (?EditorTab) => void) => React.Node,
|};
/**
* The titlebar containing a menu, the tabs and giving space for window controls.
*/
export default function TabsTitlebar({
children,
toggleProjectManager,
hidden,
renderTabs,
}: TabsTitlebarProps) {
const isTouchscreen = useScreenType() === 'touch';
const gdevelopTheme = React.useContext(GDevelopThemeContext);
const backgroundColor = gdevelopTheme.titlebar.backgroundColor;
const [tooltipData, setTooltipData] = React.useState<?{|
element: HTMLElement,
editorTab: EditorTab,
|}>(null);
const tooltipTimeoutId = React.useRef<?TimeoutID>(null);
React.useEffect(
() => {
@@ -53,6 +64,51 @@ export default function TabsTitlebar({
[backgroundColor]
);
const onHoverEditorTab = React.useCallback(
(editorTab: ?EditorTab) => {
if (isTouchscreen) {
setTooltipData(null);
return;
}
if (tooltipTimeoutId.current) {
clearTimeout(tooltipTimeoutId.current);
tooltipTimeoutId.current = null;
}
if (editorTab) {
const element = document.getElementById(getTabId(editorTab));
if (element) {
tooltipTimeoutId.current = setTimeout(
() => {
setTooltipData({ editorTab, element });
},
// If the tooltip is already displayed, quickly change to the new tab
// but not too quick because the display might look flickering.
tooltipData ? 100 : 500
);
}
} else {
tooltipTimeoutId.current = setTimeout(() => {
setTooltipData(null);
}, 50);
}
},
[isTouchscreen, tooltipData]
);
React.useEffect(
() => {
return () => {
if (tooltipTimeoutId.current) {
clearTimeout(tooltipTimeoutId.current);
}
};
},
// Clear timeout if necessary when unmounting.
[]
);
return (
<div
style={{
@@ -77,8 +133,14 @@ export default function TabsTitlebar({
>
<MenuIcon />
</IconButton>
{children}
{renderTabs(onHoverEditorTab)}
<TitleBarRightSafeMargins />
{tooltipData && (
<TabsTitlebarTooltip
anchorElement={tooltipData.element}
editorTab={tooltipData.editorTab}
/>
)}
</div>
);
}

View File

@@ -0,0 +1,162 @@
// @flow
import * as React from 'react';
import { Trans } from '@lingui/macro';
import Fade from '@material-ui/core/Fade';
import Paper from '@material-ui/core/Paper';
import Popper from '@material-ui/core/Popper';
import GDevelopThemeContext from '../UI/Theme/GDevelopThemeContext';
import {
getEditorTabMetadata,
type EditorTab,
type EditorKind,
} from './EditorTabs/EditorTabsHandler';
import { ColumnStackLayout } from '../UI/Layout';
import { Line } from '../UI/Grid';
import Text from '../UI/Text';
const editorKindToLabel: { [kind: EditorKind]: React.Node } = {
layout: <Trans>Scene</Trans>,
'layout events': <Trans>Scene events</Trans>,
'external layout': <Trans>External layout</Trans>,
'external events': <Trans>External events</Trans>,
'events functions extension': <Trans>Extension</Trans>,
'custom object': <Trans>Object</Trans>,
debugger: <Trans>Debugger</Trans>,
resources: <Trans>Resources</Trans>,
'start page': <Trans>Homepage</Trans>,
};
const styles = {
paper: {
padding: '8px 10px',
minWidth: 180,
},
tabIcon: {
marginLeft: 4,
marginRight: 4,
display: 'flex',
},
emptyTabIcon: {
marginLeft: 4,
marginRight: 4,
height: 20,
width: 24,
display: 'flex',
},
tooltip: {
zIndex: 3,
maxWidth: 'min(90%, 300px)',
},
};
type Props = {|
anchorElement: HTMLElement,
editorTab: EditorTab,
|};
const TabsTitlebarTooltip = ({ anchorElement, editorTab }: Props) => {
const gdevelopTheme = React.useContext(GDevelopThemeContext);
const [tooltipStyle, setTooltipStyle] = React.useState<Object>(
styles.tooltip
);
const brightness = gdevelopTheme.palette.type === 'dark' ? 0.978 : 0.224;
const editorTabMetadata = getEditorTabMetadata(editorTab);
React.useEffect(
() => {
const timeoutId = setTimeout(() => {
setTooltipStyle(currentStyle => ({
...currentStyle,
transition: 'transform 150ms ease-in-out',
}));
}, 100);
return () => clearTimeout(timeoutId);
},
// Apply transition after first display of tooltip to avoid having the
// transition weirdly applied from the 0;0 coordinates.
[]
);
const title = editorKindToLabel[editorTabMetadata.editorKind];
let subtitle = null;
if (
[
'layout',
'layout events',
'external layout',
'external events',
'events functions extension',
].includes(editorTabMetadata.editorKind)
) {
subtitle = editorTabMetadata.projectItemName;
} else if (
editorTabMetadata.editorKind === 'custom object' &&
editorTabMetadata.projectItemName
) {
const nameParts = editorTabMetadata.projectItemName.split('::');
const customObjectName = nameParts[1];
if (customObjectName) {
subtitle = customObjectName;
}
}
return (
<Popper
id="tabs-titlebar-tooltip"
open={true}
anchorEl={anchorElement}
transition
placement={'bottom-start'}
popperOptions={{
modifiers: {
arrow: { enabled: false },
offset: {
enabled: true,
offset: '0,5',
},
preventOverflow: {
enabled: true,
boundariesElement: document.querySelector('.main-frame'),
},
},
}}
style={tooltipStyle}
>
{({ TransitionProps }) => (
<Fade {...TransitionProps} timeout={{ enter: 350, exit: 0 }}>
<Paper
style={{
...styles.paper,
backgroundColor: gdevelopTheme.paper.backgroundColor.light,
}}
elevation={4}
>
<ColumnStackLayout noMargin>
<Line alignItems="center" noMargin>
{editorTab.icon || editorTab.renderCustomIcon ? (
<span style={styles.tabIcon}>
{editorTab.renderCustomIcon
? editorTab.renderCustomIcon(brightness)
: editorTab.icon}
</span>
) : null}
<Text noMargin>{title}</Text>
</Line>
{subtitle && (
<Line alignItems="center" noMargin>
<span style={styles.emptyTabIcon} />
<Text noMargin>{subtitle}</Text>
</Line>
)}
</ColumnStackLayout>
</Paper>
</Fade>
)}
</Popper>
);
};
export default TabsTitlebarTooltip;

View File

@@ -3697,22 +3697,24 @@ const MainFrame = (props: Props) => {
<TabsTitlebar
hidden={tabsTitleBarAndEditorToolbarHidden}
toggleProjectManager={toggleProjectManager}
>
<DraggableEditorTabs
hideLabels={false}
editorTabs={state.editorTabs}
onClickTab={(id: number) => _onChangeEditorTab(id)}
onCloseTab={(editorTab: EditorTab) => _onCloseEditorTab(editorTab)}
onCloseOtherTabs={(editorTab: EditorTab) =>
_onCloseOtherEditorTabs(editorTab)
}
onCloseAll={_onCloseAllEditorTabs}
onTabActivated={(editorTab: EditorTab) =>
_onEditorTabActivated(editorTab)
}
onDropTab={onDropEditorTab}
/>
</TabsTitlebar>
renderTabs={onHoverEditorTab => (
<DraggableEditorTabs
hideLabels={false}
editorTabs={state.editorTabs}
onClickTab={(id: number) => _onChangeEditorTab(id)}
onCloseTab={(editorTab: EditorTab) => _onCloseEditorTab(editorTab)}
onCloseOtherTabs={(editorTab: EditorTab) =>
_onCloseOtherEditorTabs(editorTab)
}
onCloseAll={_onCloseAllEditorTabs}
onTabActivated={(editorTab: EditorTab) =>
_onEditorTabActivated(editorTab)
}
onDropTab={onDropEditorTab}
onHoverTab={onHoverEditorTab}
/>
)}
/>
<Toolbar
ref={toolbar}
hidden={tabsTitleBarAndEditorToolbarHidden}

View File

@@ -140,6 +140,7 @@ export type ClosableTabProps = {|
onCloseAll: () => void,
onClick: () => void,
onActivated: () => void,
onHover: boolean => void,
maxWidth: number,
|};
@@ -156,6 +157,7 @@ export function ClosableTab({
closable,
onClick,
onActivated,
onHover,
maxWidth,
}: ClosableTabProps) {
React.useEffect(
@@ -248,6 +250,8 @@ export function ClosableTab({
// A tab lives in the top bar, which has the ability to drag the app window.
// Ensure the tab does not have this ability, as it can be dragged itself.
className={WINDOW_NON_DRAGGABLE_PART_CLASS_NAME}
onMouseEnter={() => onHover(true)}
onMouseLeave={() => onHover(false)}
>
<ButtonBase
onClick={onClick}
@@ -281,7 +285,6 @@ export function ClosableTab({
...styles.tabLabel,
maxWidth: labelMaxWidth,
}}
title={label}
>
{label}
</span>

View File

@@ -36,6 +36,7 @@ export const ThreeTabs = () => (
<>
<ClosableTab
onActivated={action('Tab 1 activated')}
onHover={action('onHover')}
closable={false}
active={value === 0}
onClick={() => onChange(0)}
@@ -48,6 +49,7 @@ export const ThreeTabs = () => (
/>
<ClosableTab
onActivated={action('Tab 2 activated')}
onHover={action('onHover')}
closable
active={value === 1}
onClick={() => onChange(1)}
@@ -60,6 +62,7 @@ export const ThreeTabs = () => (
/>
<ClosableTab
onActivated={action('Tab 3 activated')}
onHover={action('onHover')}
closable
active={value === 2}
onClick={() => onChange(2)}
@@ -119,6 +122,7 @@ export const LongLabels = () => (
<>
<ClosableTab
onActivated={action('Tab 1 activated')}
onHover={action('onHover')}
closable
active={value === 0}
label="Tab 1 with a very very long label"
@@ -131,6 +135,7 @@ export const LongLabels = () => (
/>
<ClosableTab
onActivated={action('Tab 2 activated')}
onHover={action('onHover')}
closable
active={value === 1}
onClick={() => onChange(1)}
@@ -143,6 +148,7 @@ export const LongLabels = () => (
/>
<ClosableTab
onActivated={action('Tab 3 activated')}
onHover={action('onHover')}
closable
active={value === 2}
onClick={() => onChange(2)}
@@ -155,6 +161,7 @@ export const LongLabels = () => (
/>
<ClosableTab
onActivated={action('Tab 4 activated')}
onHover={action('onHover')}
closable
active={value === 3}
onClick={() => onChange(3)}
@@ -232,6 +239,7 @@ export const WithObjectsList = () => (
renderTabs={({ containerWidth }) => [
<ClosableTab
onActivated={action('Tab 1 activated')}
onHover={action('onHover')}
closable
active={value === 0}
label="Tab 1"
@@ -244,6 +252,7 @@ export const WithObjectsList = () => (
/>,
<ClosableTab
onActivated={action('Tab 2 activated')}
onHover={action('onHover')}
closable
active={value === 1}
label="Tab 2"
@@ -256,6 +265,7 @@ export const WithObjectsList = () => (
/>,
<ClosableTab
onActivated={action('Tab 3 activated')}
onHover={action('onHover')}
closable
active={value === 2}
label="Tab 3"