refactor(component): refactor the implementation of Button and IconButton (#7716)

## Button
- Remove props withoutHoverStyle
   refactor hover impl with independent layer, so that hover-color won't affect the background even if is overridden outside
- Update `type` (renamed to `variant`):
  - remove `processing` and `warning`
  - rename `default` with `secondary`
- Remove `shape` props
- Remove `icon` and `iconPosition`, replaced with `prefix: ReactNode` and `suffix: ReactNode`
- Integrate tooltip for more convenient usage
- New Storybook document
- Focus style

## IconButton
- A Wrapper base on `<Button />`
- Override Button size and variant
  - size: `'12' | '14' | '16' | '20' | '24' | number`
     These presets size are referenced from the design system.
  - variant:  `'plain' | 'solid' | 'danger' | 'custom'`
- Inset icon via Button 's prefix

## Fix
- fix some button related issues
- close AF-1159, AF-1160, AF-1161, AF-1162, AF-1163, AF-1158, AF-1157

## Storybook

![CleanShot 2024-08-03 at 14.57.20@2x.png](https://graphite-user-uploaded-assets-prod.s3.amazonaws.com/LakojjjzZNf6ogjOVwKE/f5a76110-35d0-4082-a940-efc12bed87b0.png)
This commit is contained in:
CatsJuice
2024-08-05 02:57:23 +00:00
parent 10deed94e3
commit 3d855647c7
159 changed files with 1384 additions and 1539 deletions
@@ -362,11 +362,15 @@ export const AppTabsHeader = ({
ref={spacerDropTargetRef}
data-dragged-over={draggedOver}
>
<IconButton onClick={onAddTab} data-testid="add-tab-view-button">
<PlusIcon />
</IconButton>
<IconButton
size={22.86}
onClick={onAddTab}
data-testid="add-tab-view-button"
style={{ width: 32, height: 32 }}
icon={<PlusIcon />}
/>
</div>
<IconButton size="large" onClick={onToggleRightSidebar}>
<IconButton size="24" onClick={onToggleRightSidebar}>
<RightSidebarIcon />
</IconButton>
{environment.isDesktop && environment.isWindows ? (
@@ -95,7 +95,7 @@ export const useExplorerCollectionNodeOperations = (
cancelText: t['Cancel'](),
confirmText: t['Confirm'](),
confirmButtonOptions: {
type: 'primary',
variant: 'primary',
},
onConfirm: createAndAddDocument,
});
@@ -129,11 +129,7 @@ export const useExplorerCollectionNodeOperations = (
index: 0,
inline: true,
view: (
<IconButton
size="small"
type="plain"
onClick={handleAddDocToCollection}
>
<IconButton size="16" onClick={handleAddDocToCollection}>
<PlusIcon />
</IconButton>
),
@@ -61,7 +61,7 @@ export const useExplorerDocNodeOperations = (
confirmText: t['com.affine.moveToTrash.confirmModal.confirm'](),
cancelText: t['com.affine.moveToTrash.confirmModal.cancel'](),
confirmButtonOptions: {
type: 'error',
variant: 'error',
},
onConfirm() {
docRecord.moveToTrash();
@@ -633,7 +633,7 @@ export const ExplorerFolderNodeFolder = ({
index: 0,
inline: true,
view: (
<IconButton size="small" type="plain" onClick={handleNewDoc}>
<IconButton size="16" onClick={handleNewDoc}>
<PlusIcon />
</IconButton>
),
@@ -103,7 +103,7 @@ export const useExplorerTagNodeOperations = (
index: 0,
inline: true,
view: (
<IconButton size="small" type="plain" onClick={handleNewDoc}>
<IconButton size="16" onClick={handleNewDoc}>
<PlusIcon />
</IconButton>
),
@@ -63,7 +63,7 @@ export const ExplorerCollections = () => {
<IconButton
data-testid="explorer-bar-add-collection-button"
onClick={handleCreateCollection}
size="small"
size="16"
>
<PlusIcon />
</IconButton>
@@ -239,7 +239,7 @@ export const ExplorerFavorites = () => {
<IconButton
data-testid="explorer-bar-add-favorite-button"
onClick={handleCreateNewFavoriteDoc}
size="small"
size="16"
>
<PlusIcon />
</IconButton>
@@ -52,7 +52,7 @@ export const ExplorerMigrationFavorites = () => {
confirmText:
t['com.affine.rootAppSidebar.migration-data.clean-all.confirm'](),
confirmButtonOptions: {
type: 'primary',
variant: 'primary',
},
cancelText:
t['com.affine.rootAppSidebar.migration-data.clean-all.cancel'](),
@@ -79,13 +79,12 @@ export const ExplorerMigrationFavorites = () => {
t['com.affine.rootAppSidebar.migration-data.help.description'](),
confirmText: t['com.affine.rootAppSidebar.migration-data.help.confirm'](),
confirmButtonOptions: {
type: 'primary',
variant: 'primary',
},
cancelText:
t['com.affine.rootAppSidebar.migration-data.help.clean-all'](),
cancelButtonOptions: {
icon: <BroomIcon />,
type: 'default',
prefix: <BroomIcon />,
onClick: () => {
requestAnimationFrame(() => {
handleClickClear();
@@ -114,13 +113,13 @@ export const ExplorerMigrationFavorites = () => {
<IconButton
data-testid="explorer-bar-favorite-migration-clear-button"
onClick={handleClickClear}
size="small"
size="16"
>
<BroomIcon />
</IconButton>
<IconButton
data-testid="explorer-bar-favorite-migration-help-button"
size="small"
size="16"
onClick={handleClickHelp}
>
<HelpIcon />
@@ -184,7 +184,7 @@ export const ExplorerOldFavorites = () => {
<IconButton
data-testid="explorer-bar-add-old-favorite-button"
onClick={handleCreateNewFavoriteDoc}
size="small"
size="16"
>
<PlusIcon />
</IconButton>
@@ -122,7 +122,7 @@ export const ExplorerOrganize = () => {
<IconButton
data-testid="explorer-bar-add-organize-button"
onClick={handleCreateFolder}
size="small"
size="16"
>
<PlusIcon />
</IconButton>
@@ -53,7 +53,7 @@ export const ExplorerTags = () => {
<IconButton
data-testid="explorer-bar-add-favorite-button"
onClick={handleCreateNewFavoriteDoc}
size="small"
size="16"
>
<PlusIcon />
</IconButton>
@@ -344,8 +344,7 @@ export const ExplorerTreeNode = ({
))}
>
<IconButton
size="small"
type="plain"
size="16"
data-testid="explorer-tree-node-operation-button"
style={{ marginLeft: 4 }}
>
@@ -1,4 +1,5 @@
import { cssVar } from '@toeverything/theme';
import { cssVarV2 } from '@toeverything/theme/v2';
import { createVar, keyframes, style } from '@vanilla-extract/css';
export const animationTimeout = createVar();
@@ -52,6 +53,7 @@ export const modalContent = style({
// :focus-visible will set outline
outline: 'none',
display: 'flex',
gap: 8,
alignItems: 'center',
justifyContent: 'space-between',
border: `0.5px solid ${cssVar('borderColor')}`,
@@ -133,22 +135,13 @@ export const count = style({
});
export const arrowButton = style({
padding: '4px',
fontSize: '24px',
width: '32px',
height: '32px',
width: 32,
height: 32,
flexShrink: 0,
border: '1px solid',
borderColor: cssVar('borderColor'),
color: cssVar('iconSecondary'),
alignItems: 'baseline',
background: 'transparent',
borderColor: cssVarV2('layer/border'),
selectors: {
'&:hover': {
color: cssVar('iconColor'),
},
'&.backward': {
marginLeft: '8px',
borderRadius: '4px 0 0 4px',
},
'&.forward': {
@@ -157,12 +150,3 @@ export const arrowButton = style({
},
},
});
export const closeButton = style({
padding: '4px',
fontSize: '20px',
width: '24px',
height: '24px',
flexShrink: 0,
color: cssVar('iconColor'),
marginLeft: '8px',
});
@@ -1,4 +1,4 @@
import { Button, IconButton, observeResize } from '@affine/component';
import { IconButton, observeResize } from '@affine/component';
import {
ArrowDownSmallIcon,
ArrowUpSmallIcon,
@@ -182,63 +182,58 @@ export const FindInPageModal = () => {
className={styles.modalContent}
data-state={status}
>
<div className={styles.leftContent}>
<div
className={clsx(styles.inputContainer, {
active: active,
})}
>
<SearchIcon className={styles.searchIcon} />
<div className={styles.inputMain}>
<input
type="text"
autoFocus
value={value}
ref={inputRef}
style={{
visibility: isSearching ? 'hidden' : 'visible',
}}
onBlur={handleBlur}
onFocus={handleFocus}
className={styles.input}
onKeyDown={handleKeydown}
onChange={e => handleValueChange(e.target.value)}
/>
<CanvasText className={styles.inputHack} text={value} />
</div>
<div className={styles.count}>
{value.length > 0 && result && result.matches !== 0 ? (
<>
<span>{result?.activeMatchOrdinal || 0}</span>
<span>/</span>
<span>{result?.matches || 0}</span>
</>
) : value.length ? (
<span>No matches</span>
) : null}
</div>
<div
className={clsx(styles.inputContainer, {
active: active,
})}
>
<SearchIcon className={styles.searchIcon} />
<div className={styles.inputMain}>
<input
type="text"
autoFocus
value={value}
ref={inputRef}
style={{
visibility: isSearching ? 'hidden' : 'visible',
}}
onBlur={handleBlur}
onFocus={handleFocus}
className={styles.input}
onKeyDown={handleKeydown}
onChange={e => handleValueChange(e.target.value)}
/>
<CanvasText className={styles.inputHack} text={value} />
</div>
<div className={styles.count}>
{value.length > 0 && result && result.matches !== 0 ? (
<>
<span>{result?.activeMatchOrdinal || 0}</span>
<span>/</span>
<span>{result?.matches || 0}</span>
</>
) : value.length ? (
<span>No matches</span>
) : null}
</div>
</div>
<Button
<div>
<IconButton
size="24"
className={clsx(styles.arrowButton, 'backward')}
onClick={handleBackWard}
>
<ArrowUpSmallIcon />
</Button>
<Button
icon={<ArrowUpSmallIcon />}
/>
<IconButton
size="24"
className={clsx(styles.arrowButton, 'forward')}
onClick={handleForward}
>
<ArrowDownSmallIcon />
</Button>
icon={<ArrowDownSmallIcon />}
/>
</div>
<IconButton
className={styles.closeButton}
type="plain"
onClick={handleDone}
>
<CloseIcon />
</IconButton>
<IconButton onClick={handleDone} icon={<CloseIcon />} />
</Dialog.Content>
</div>
</Dialog.Portal>
@@ -1,4 +1,4 @@
import { IconButton, Tooltip } from '@affine/component';
import { IconButton } from '@affine/component';
import { useI18n } from '@affine/i18n';
import { ArrowLeftSmallIcon, ArrowRightSmallIcon } from '@blocksuite/icons/rc';
import { useLiveData, useService } from '@toeverything/infra';
@@ -8,6 +8,8 @@ import { useGeneralShortcuts } from '../../../hooks/affine/use-shortcuts';
import { NavigatorService } from '../services/navigator';
import * as styles from './navigation-buttons.css';
const tooltipSideBottom = { side: 'bottom' as const };
export const NavigationButtons = () => {
const t = useI18n();
@@ -63,34 +65,29 @@ export const NavigationButtons = () => {
return null;
}
// TODO(@CatsJuice): tooltip with shortcut
return (
<div className={styles.container}>
<Tooltip
content={`${shortcutsObject.goBack} ${shortcutsObject.goBackShortcut}`}
side="bottom"
<IconButton
tooltip={`${shortcutsObject.goBack} ${shortcutsObject.goBackShortcut}`}
tooltipOptions={tooltipSideBottom}
className={styles.button}
data-testid="app-navigation-button-back"
disabled={!backable}
onClick={handleBack}
>
<IconButton
className={styles.button}
data-testid="app-navigation-button-back"
disabled={!backable}
onClick={handleBack}
>
<ArrowLeftSmallIcon />
</IconButton>
</Tooltip>
<Tooltip
content={`${shortcutsObject.goForward} ${shortcutsObject.goForwardShortcut}`}
side="bottom"
<ArrowLeftSmallIcon />
</IconButton>
<IconButton
tooltip={`${shortcutsObject.goForward} ${shortcutsObject.goForwardShortcut}`}
tooltipOptions={tooltipSideBottom}
className={styles.button}
data-testid="app-navigation-button-forward"
disabled={!forwardable}
onClick={handleForward}
>
<IconButton
className={styles.button}
data-testid="app-navigation-button-forward"
disabled={!forwardable}
onClick={handleForward}
>
<ArrowRightSmallIcon />
</IconButton>
</Tooltip>
<ArrowRightSmallIcon />
</IconButton>
</div>
);
};
@@ -1,6 +1,5 @@
import { toast } from '@affine/component';
import { Button, IconButton } from '@affine/component/ui/button';
import { Tooltip } from '@affine/component/ui/tooltip';
import { useAsyncCallback } from '@affine/core/hooks/affine-async-hooks';
import type { ImageBlockModel } from '@blocksuite/blocks';
import { assertExists } from '@blocksuite/global/utils';
@@ -100,30 +99,6 @@ export type ImagePreviewModalProps = {
blockId: string;
};
const ButtonWithTooltip = ({
icon,
tooltip,
disabled,
...props
}: PropsWithChildren<{
disabled?: boolean;
icon?: ReactElement;
tooltip: string;
onClick: () => void;
className?: string;
}>) => {
const element = icon ? (
<IconButton icon={icon} type="plain" disabled={disabled} {...props} />
) : (
<Button disabled={disabled} type="plain" {...props} />
);
if (disabled) {
return element;
} else {
return <Tooltip content={tooltip}>{element}</Tooltip>;
}
};
const ImagePreviewModalImpl = ({
docId,
blockId,
@@ -362,7 +337,7 @@ const ImagePreviewModalImpl = ({
</p>
) : null}
<div className={styles.imagePreviewActionBarStyle}>
<ButtonWithTooltip
<IconButton
data-testid="previous-image-button"
tooltip="Previous"
icon={<ArrowLeftSmallIcon />}
@@ -372,7 +347,7 @@ const ImagePreviewModalImpl = ({
<div className={styles.cursorStyle}>
{`${blocks.length ? cursor + 1 : 0}/${blocks.length}`}
</div>
<ButtonWithTooltip
<IconButton
data-testid="next-image-button"
tooltip="Next"
icon={<ArrowRightSmallIcon />}
@@ -380,40 +355,41 @@ const ImagePreviewModalImpl = ({
onClick={() => goto(cursor + 1)}
/>
<div className={styles.dividerStyle}></div>
<ButtonWithTooltip
<IconButton
data-testid="fit-to-screen-button"
tooltip="Fit to screen"
icon={<ViewBarIcon />}
onClick={() => resetZoom()}
/>
<ButtonWithTooltip
<IconButton
data-testid="zoom-out-button"
tooltip="Zoom out"
icon={<MinusIcon />}
onClick={zoomOut}
/>
<ButtonWithTooltip
<Button
data-testid="reset-scale-button"
tooltip="Reset scale"
onClick={resetScale}
variant="plain"
>
{`${(currentScale * 100).toFixed(0)}%`}
</ButtonWithTooltip>
</Button>
<ButtonWithTooltip
<IconButton
data-testid="zoom-in-button"
tooltip="Zoom in"
icon={<PlusIcon />}
onClick={zoomIn}
/>
<div className={styles.dividerStyle}></div>
<ButtonWithTooltip
<IconButton
data-testid="download-button"
tooltip="Download"
icon={<DownloadIcon />}
onClick={downloadHandler}
/>
<ButtonWithTooltip
<IconButton
data-testid="copy-to-clipboard-button"
tooltip="Copy to clipboard"
icon={<CopyIcon />}
@@ -422,13 +398,13 @@ const ImagePreviewModalImpl = ({
{blockModel && !blockModel.doc.readonly && (
<>
<div className={styles.dividerStyle}></div>
<ButtonWithTooltip
className="danger"
<IconButton
data-testid="delete-button"
tooltip="Delete"
icon={<DeleteIcon />}
disabled={blocks.length === 0}
onClick={() => deleteHandler(cursor)}
variant="danger"
/>
</>
)}
@@ -1,4 +1,3 @@
import { cssVar } from '@toeverything/theme';
import { style } from '@vanilla-extract/css';
export const root = style({
@@ -9,12 +8,7 @@ export const root = style({
});
export const button = style({
color: cssVar('iconColor'),
boxShadow: cssVar('shadow2'),
borderRadius: 8,
fontSize: '20px !important',
':hover': {
background: cssVar('hoverColorFilled'),
},
pointerEvents: 'auto',
width: 32,
height: 32,
});
@@ -1,4 +1,4 @@
import { IconButton, Tooltip } from '@affine/component';
import { IconButton } from '@affine/component';
import { useNavigateHelper } from '@affine/core/hooks/use-navigate-helper';
import { useI18n } from '@affine/i18n';
import {
@@ -45,18 +45,16 @@ export const ControlButton = ({
);
return (
<Tooltip content={name}>
<IconButton
data-testid="peek-view-control"
data-action-name={nameKey}
size="large"
type="default"
onClick={handleClick}
icon={icon}
className={styles.button}
withoutHoverStyle
/>
</Tooltip>
<IconButton
variant="solid"
tooltip={name}
data-testid="peek-view-control"
data-action-name={nameKey}
size="20"
onClick={handleClick}
icon={icon}
className={styles.button}
/>
);
};
@@ -56,7 +56,7 @@ export const DeleteTagConfirmModal = ({
}
confirmText={t['Delete']()}
confirmButtonOptions={{
type: 'warning',
variant: 'error',
}}
onConfirm={handleDelete}
/>
@@ -30,7 +30,7 @@ const ToggleButton = ({
}) => {
return (
<IconButton
size="large"
size="24"
onClick={onToggle}
className={className}
data-show={show}
@@ -33,7 +33,7 @@ export const SidebarHeaderSwitcher = () => {
[view]
);
return (
return tabItems.length ? (
<RadioGroup
borderRadius={8}
itemHeight={24}
@@ -44,5 +44,5 @@ export const SidebarHeaderSwitcher = () => {
onChange={handleActiveTabChange}
activeItemStyle={{ color: cssVar('primaryColor') }}
/>
);
) : null;
};
@@ -34,7 +34,7 @@ function Container({
const ToggleButton = ({ onToggle }: { onToggle?: () => void }) => {
return (
<IconButton size="large" onClick={onToggle}>
<IconButton size="24" onClick={onToggle}>
<RightSidebarIcon />
</IconButton>
);