mirror of
https://github.com/toeverything/AFFiNE.git
synced 2024-12-02 14:33:54 +03:00
feat(component): shortcut style for tooltip (#7721)
![image.png](https://graphite-user-uploaded-assets-prod.s3.amazonaws.com/LakojjjzZNf6ogjOVwKE/2e68337c-91f1-4ea7-8426-7fb33be02081.png) - New `shortcut` prop for `<Tooltip />` - single key ```tsx <Tooltip shortcut="T" /> ``` - multiple ```tsx <Tooltip shortcut={["⌘", "K"]} /> ``` - Round tooltip's arrow - Use new design system colors - Replace some usage - App sidebar switch - Editor mode switch - New tab (new)
This commit is contained in:
parent
3d855647c7
commit
249f3471c9
@ -64,7 +64,8 @@ export interface ButtonProps
|
||||
suffixStyle?: CSSProperties;
|
||||
|
||||
tooltip?: TooltipProps['content'];
|
||||
tooltipOptions?: Partial<Omit<TooltipProps, 'content'>>;
|
||||
tooltipShortcut?: TooltipProps['shortcut'];
|
||||
tooltipOptions?: Partial<Omit<TooltipProps, 'content' | 'shortcut'>>;
|
||||
}
|
||||
|
||||
const IconSlot = ({
|
||||
@ -113,6 +114,7 @@ export const Button = forwardRef<HTMLButtonElement, ButtonProps>(
|
||||
contentStyle,
|
||||
|
||||
tooltip,
|
||||
tooltipShortcut,
|
||||
tooltipOptions,
|
||||
onClick,
|
||||
|
||||
@ -129,7 +131,7 @@ export const Button = forwardRef<HTMLButtonElement, ButtonProps>(
|
||||
);
|
||||
|
||||
return (
|
||||
<Tooltip content={tooltip} {...tooltipOptions}>
|
||||
<Tooltip content={tooltip} shortcut={tooltipShortcut} {...tooltipOptions}>
|
||||
<button
|
||||
{...otherProps}
|
||||
ref={ref}
|
||||
|
@ -1,11 +1,46 @@
|
||||
import { cssVar } from '@toeverything/theme';
|
||||
import { cssVarV2 } from '@toeverything/theme/v2';
|
||||
import { style } from '@vanilla-extract/css';
|
||||
export const tooltipContent = style({
|
||||
backgroundColor: cssVar('tooltip'),
|
||||
color: cssVar('white'),
|
||||
backgroundColor: cssVarV2('tooltips/background'),
|
||||
color: cssVarV2('tooltips/foreground'),
|
||||
padding: '5px 12px',
|
||||
fontSize: cssVar('fontSm'),
|
||||
lineHeight: '22px',
|
||||
borderRadius: '4px',
|
||||
maxWidth: '280px',
|
||||
});
|
||||
|
||||
export const withShortcut = style({
|
||||
display: 'flex',
|
||||
gap: 10,
|
||||
});
|
||||
export const withShortcutContent = style({
|
||||
whiteSpace: 'nowrap',
|
||||
textOverflow: 'ellipsis',
|
||||
overflow: 'hidden',
|
||||
});
|
||||
|
||||
export const shortcut = style({
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 2,
|
||||
});
|
||||
export const command = style({
|
||||
background: cssVarV2('tooltips/secondaryBackground'),
|
||||
fontSize: cssVar('fontXs'),
|
||||
fontWeight: 400,
|
||||
lineHeight: '20px',
|
||||
height: 16,
|
||||
minWidth: 16,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
padding: '0 4px',
|
||||
borderRadius: 4,
|
||||
selectors: {
|
||||
'&[data-length="1"]': {
|
||||
width: 16,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
@ -1,6 +1,8 @@
|
||||
import type { Meta, StoryFn } from '@storybook/react';
|
||||
import { useState } from 'react';
|
||||
|
||||
import { Button } from '../button';
|
||||
import { RadioGroup } from '../radio';
|
||||
import type { TooltipProps } from './index';
|
||||
import Tooltip from './index';
|
||||
|
||||
@ -18,6 +20,77 @@ const Template: StoryFn<TooltipProps> = args => (
|
||||
export const Default: StoryFn<TooltipProps> = Template.bind(undefined);
|
||||
Default.args = {};
|
||||
|
||||
export const WithShortCut = () => {
|
||||
const shortCuts = [
|
||||
['Text', 'T'],
|
||||
['Bold', ['⌘', 'B']],
|
||||
['Quick Search', ['⌘', 'K']],
|
||||
['Share', ['⌘', 'Shift', 'S']],
|
||||
['Copy', ['$mod', '$shift', 'C']],
|
||||
] as Array<[string, string | string[]]>;
|
||||
|
||||
return (
|
||||
<div style={{ display: 'flex', gap: 4 }}>
|
||||
{shortCuts.map(([name, shortcut]) => (
|
||||
<Tooltip shortcut={shortcut} content={name} key={name}>
|
||||
<Button>{name}</Button>
|
||||
</Tooltip>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const CustomAlign = () => {
|
||||
const [align, setAlign] = useState('center' as const);
|
||||
const _ = undefined;
|
||||
const positions = [
|
||||
// [top, left, right, bottom, translateX, translateY]
|
||||
[0, 0, _, _, _, _],
|
||||
[0, '50%', _, _, '-50%', _],
|
||||
[0, _, 0, _, _, _],
|
||||
['50%', 0, _, _, _, '-50%'],
|
||||
['50%', _, 0, _, _, '-50%'],
|
||||
[_, 0, _, 0, _, _],
|
||||
[_, '50%', _, 0, '-50%', _],
|
||||
[_, _, 0, 0, _, _],
|
||||
];
|
||||
return (
|
||||
<div>
|
||||
<RadioGroup
|
||||
items={['start', 'center', 'end']}
|
||||
value={align}
|
||||
onChange={setAlign}
|
||||
/>
|
||||
<div
|
||||
style={{
|
||||
width: '100%',
|
||||
height: 200,
|
||||
position: 'relative',
|
||||
border: '1px solid rgba(100,100,100,0.2)',
|
||||
marginTop: 40,
|
||||
}}
|
||||
>
|
||||
{positions.map(pos => {
|
||||
const key = pos.join('-');
|
||||
const style = {
|
||||
position: 'absolute',
|
||||
top: pos[0],
|
||||
left: pos[1],
|
||||
right: pos[2],
|
||||
bottom: pos[3],
|
||||
transform: `translate(${pos[4] ?? 0}, ${pos[5] ?? 0})`,
|
||||
} as const;
|
||||
return (
|
||||
<Tooltip align={align} content="This is a tooltip" key={key}>
|
||||
<Button style={style}>Show tooltip</Button>
|
||||
</Tooltip>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const WithCustomContent: StoryFn<TooltipProps> = args => (
|
||||
<Tooltip
|
||||
content={
|
||||
|
@ -4,15 +4,36 @@ import type {
|
||||
TooltipProps as RootProps,
|
||||
} from '@radix-ui/react-tooltip';
|
||||
import * as TooltipPrimitive from '@radix-ui/react-tooltip';
|
||||
import { cssVar } from '@toeverything/theme';
|
||||
import { cssVarV2 } from '@toeverything/theme/v2';
|
||||
import clsx from 'clsx';
|
||||
import type { ReactElement, ReactNode } from 'react';
|
||||
import { type ReactElement, type ReactNode } from 'react';
|
||||
|
||||
import { getCommand } from '../../utils/keyboard-mapping';
|
||||
import * as styles from './styles.css';
|
||||
|
||||
export interface TooltipProps {
|
||||
// `children` can not be string, number or even undefined
|
||||
children: ReactElement;
|
||||
content?: ReactNode;
|
||||
/**
|
||||
* When shortcut is provided, will use a single line layout
|
||||
*
|
||||
* ```tsx
|
||||
* <Tooltip shortcut="T" /> // [T]
|
||||
* <Tooltip shortcut="⌘ + K" /> // [⌘ + K]
|
||||
* <Tooltip shortcut={['⌘', 'K']} /> // [⌘] [K]
|
||||
* <Tooltip shortcut={['$mod', 'K']} /> // [⌘] [K] or [Ctrl] [K]
|
||||
* ```
|
||||
*
|
||||
* Mapping:
|
||||
* | Shortcut | macOS | Windows |
|
||||
* |----------|-------|---------|
|
||||
* | `$mod` | `⌘` | `Ctrl` |
|
||||
* | `$alt` | `⌥` | `Alt` |
|
||||
* | `$shift` | `⇧` | `Shift` |
|
||||
*/
|
||||
shortcut?: string | string[];
|
||||
side?: TooltipContentProps['side'];
|
||||
align?: TooltipContentProps['align'];
|
||||
|
||||
@ -21,11 +42,32 @@ export interface TooltipProps {
|
||||
options?: Omit<TooltipContentProps, 'side' | 'align'>;
|
||||
}
|
||||
|
||||
const TooltipShortcut = ({ shortcut }: { shortcut: string | string[] }) => {
|
||||
const commands = (Array.isArray(shortcut) ? shortcut : [shortcut])
|
||||
.map(cmd => cmd.trim())
|
||||
.map(cmd => getCommand(cmd));
|
||||
|
||||
return (
|
||||
<div className={styles.shortcut}>
|
||||
{commands.map((cmd, index) => (
|
||||
<div
|
||||
key={`${index}-${cmd}`}
|
||||
className={styles.command}
|
||||
data-length={cmd.length}
|
||||
>
|
||||
{cmd}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const Tooltip = ({
|
||||
children,
|
||||
content,
|
||||
side = 'top',
|
||||
align = 'center',
|
||||
shortcut,
|
||||
options,
|
||||
rootOptions,
|
||||
portalOptions,
|
||||
@ -34,6 +76,7 @@ export const Tooltip = ({
|
||||
return children;
|
||||
}
|
||||
const { className, ...contentOptions } = options || {};
|
||||
const { style: contentStyle, ...restContentOptions } = contentOptions;
|
||||
return (
|
||||
<TooltipPrimitive.Provider>
|
||||
<TooltipPrimitive.Root delayDuration={500} {...rootOptions}>
|
||||
@ -45,15 +88,31 @@ export const Tooltip = ({
|
||||
side={side}
|
||||
align={align}
|
||||
sideOffset={5}
|
||||
style={{ zIndex: 'var(--affine-z-index-popover)' }}
|
||||
{...contentOptions}
|
||||
style={{ zIndex: cssVar('zIndexPopover'), ...contentStyle }}
|
||||
{...restContentOptions}
|
||||
>
|
||||
{content}
|
||||
<TooltipPrimitive.Arrow
|
||||
height={6}
|
||||
width={10}
|
||||
fill="var(--affine-tooltip)"
|
||||
/>
|
||||
{shortcut ? (
|
||||
<div className={styles.withShortcut}>
|
||||
<div className={styles.withShortcutContent}>{content}</div>
|
||||
<TooltipShortcut shortcut={shortcut} />
|
||||
</div>
|
||||
) : (
|
||||
content
|
||||
)}
|
||||
<TooltipPrimitive.Arrow asChild>
|
||||
<svg
|
||||
width="10"
|
||||
height="6"
|
||||
viewBox="0 0 10 6"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M4.11111 5.55C4.55556 6.15 5.44445 6.15 5.88889 5.55L10 0H0L4.11111 5.55Z"
|
||||
fill={cssVarV2('tooltips/background')}
|
||||
/>
|
||||
</svg>
|
||||
</TooltipPrimitive.Arrow>
|
||||
</TooltipPrimitive.Content>
|
||||
</TooltipPrimitive.Portal>
|
||||
</TooltipPrimitive.Root>
|
||||
|
10
packages/frontend/component/src/utils/keyboard-mapping.ts
Normal file
10
packages/frontend/component/src/utils/keyboard-mapping.ts
Normal file
@ -0,0 +1,10 @@
|
||||
import { isMacOS } from './platform';
|
||||
|
||||
const macOS = isMacOS();
|
||||
|
||||
export const getCommand = (cmd: '$mod' | '$shift' | '$alt' | string) => {
|
||||
if (cmd === '$mod') return macOS ? '⌘' : 'Ctrl';
|
||||
if (cmd === '$alt') return macOS ? '⌥' : 'Alt';
|
||||
if (cmd === '$shift') return macOS ? '⇧' : 'Shift';
|
||||
return cmd;
|
||||
};
|
4
packages/frontend/component/src/utils/platform.ts
Normal file
4
packages/frontend/component/src/utils/platform.ts
Normal file
@ -0,0 +1,4 @@
|
||||
export function isMacOS() {
|
||||
if (typeof navigator === 'undefined') return false;
|
||||
return navigator.userAgent.indexOf('Mac') !== -1;
|
||||
}
|
@ -18,9 +18,6 @@ export const SidebarSwitch = ({
|
||||
const tooltipContent = open
|
||||
? t['com.affine.sidebarSwitch.collapse']()
|
||||
: t['com.affine.sidebarSwitch.expand']();
|
||||
// TODO(@CatsJuice): Tooltip shortcut style
|
||||
const collapseKeyboardShortcuts =
|
||||
environment.isBrowser && environment.isMacOs ? ' ⌘+/' : ' Ctrl+/';
|
||||
|
||||
return (
|
||||
<div
|
||||
@ -29,7 +26,8 @@ export const SidebarSwitch = ({
|
||||
data-testid={`app-sidebar-arrow-button-${open ? 'collapse' : 'expand'}`}
|
||||
>
|
||||
<IconButton
|
||||
tooltip={tooltipContent + ' ' + collapseKeyboardShortcuts}
|
||||
tooltip={tooltipContent}
|
||||
tooltipShortcut={['$mod', '/']}
|
||||
tooltipOptions={{ side: open ? 'bottom' : 'right' }}
|
||||
className={className}
|
||||
size="24"
|
||||
|
@ -13,7 +13,7 @@ import { useCallback, useEffect } from 'react';
|
||||
|
||||
import type { DocCollection } from '../../../shared';
|
||||
import { toast } from '../../../utils';
|
||||
import { StyledEditorModeSwitch, StyledKeyboardItem } from './style';
|
||||
import { StyledEditorModeSwitch } from './style';
|
||||
import { EdgelessSwitchItem, PageSwitchItem } from './switch-items';
|
||||
|
||||
export type EditorModeSwitchProps = {
|
||||
@ -24,17 +24,7 @@ export type EditorModeSwitchProps = {
|
||||
isPublic?: boolean;
|
||||
publicMode?: DocMode;
|
||||
};
|
||||
const TooltipContent = () => {
|
||||
const t = useI18n();
|
||||
return (
|
||||
<>
|
||||
{t['Switch']()}
|
||||
<StyledKeyboardItem>
|
||||
{!environment.isServer && environment.isMacOs ? '⌥ + S' : 'Alt + S'}
|
||||
</StyledKeyboardItem>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export const EditorModeSwitch = ({
|
||||
style,
|
||||
docCollection,
|
||||
@ -106,7 +96,9 @@ export const EditorModeSwitch = ({
|
||||
|
||||
return (
|
||||
<Tooltip
|
||||
content={<TooltipContent />}
|
||||
content={t['Switch']()}
|
||||
shortcut={['$alt', 'S']}
|
||||
side="bottom"
|
||||
options={{
|
||||
hidden: isPublic || trash,
|
||||
}}
|
||||
|
@ -1,5 +1,6 @@
|
||||
import { displayFlex, styled } from '@affine/component';
|
||||
|
||||
// TODO(@CatsJuice): refactor this component
|
||||
export const StyledEditorModeSwitch = styled('div')<{
|
||||
switchLeft: boolean;
|
||||
showAlone?: boolean;
|
||||
@ -59,14 +60,3 @@ export const StyledSwitchItem = styled('button')<{
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
export const StyledKeyboardItem = styled('span')(() => {
|
||||
return {
|
||||
marginLeft: '10px',
|
||||
fontSize: 'var(--affine-font-xs)',
|
||||
paddingLeft: '5px',
|
||||
paddingRight: '5px',
|
||||
backgroundColor: 'var(--affine-white-10)',
|
||||
borderRadius: '4px',
|
||||
};
|
||||
});
|
||||
|
@ -16,6 +16,7 @@ import { WindowsAppControls } from '@affine/core/components/pure/header/windows-
|
||||
import { useAsyncCallback } from '@affine/core/hooks/affine-async-hooks';
|
||||
import type { AffineDNDData } from '@affine/core/types/dnd';
|
||||
import { apis, events } from '@affine/electron-api';
|
||||
import { useI18n } from '@affine/i18n';
|
||||
import { CloseIcon, PlusIcon, RightSidebarIcon } from '@blocksuite/icons/rc';
|
||||
import {
|
||||
useLiveData,
|
||||
@ -223,6 +224,7 @@ export const AppTabsHeader = ({
|
||||
className?: string;
|
||||
left?: ReactNode;
|
||||
}) => {
|
||||
const t = useI18n();
|
||||
const sidebarWidth = useAtomValue(appSidebarWidthAtom);
|
||||
const sidebarOpen = useAtomValue(appSidebarOpenAtom);
|
||||
const sidebarFloating = useAtomValue(appSidebarFloatingAtom);
|
||||
@ -365,6 +367,8 @@ export const AppTabsHeader = ({
|
||||
<IconButton
|
||||
size={22.86}
|
||||
onClick={onAddTab}
|
||||
tooltip={t['com.affine.multi-tab.new-tab']()}
|
||||
tooltipShortcut={['$mod', 'T']}
|
||||
data-testid="add-tab-view-button"
|
||||
style={{ width: 32, height: 32 }}
|
||||
icon={<PlusIcon />}
|
||||
|
@ -36,7 +36,7 @@ export const tabs = style({
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
padding: '0 8px',
|
||||
paddingLeft: 8,
|
||||
overflow: 'clip',
|
||||
height: '100%',
|
||||
selectors: {
|
||||
|
@ -1417,5 +1417,6 @@
|
||||
"com.affine.ai.template-insert.failed": "Failed to insert template, please try again.",
|
||||
"com.affine.selector-doc.search-placeholder": "Search docs...",
|
||||
"com.affine.selector-collection.search.placeholder": "Search collections...",
|
||||
"com.affine.selector-tag.search.placeholder": "Search tags..."
|
||||
"com.affine.selector-tag.search.placeholder": "Search tags...",
|
||||
"com.affine.multi-tab.new-tab": "New Tab"
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user