refactor: show button title with tooltip on action icon hover (#6761)

* refactor: show button title with tooltip on action icon hover

* fix: test

* fix: style improve

* fix: filter action should onlyicon

* fix: test

* fix: test

* style: link action style improve
This commit is contained in:
Katherine 2025-04-27 19:51:10 +08:00 committed by GitHub
parent c2521a04c1
commit 14e6ccca01
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 46 additions and 11 deletions

View File

@ -53,14 +53,33 @@ export const filterActionSettings = new SchemaSettings({
default: fieldSchema?.['x-component-props']?.icon, default: fieldSchema?.['x-component-props']?.icon,
'x-component-props': {}, 'x-component-props': {},
}, },
onlyIcon: {
'x-decorator': 'FormItem',
'x-component': 'Checkbox',
title: t('Icon only'),
default: fieldSchema?.['x-component-props']?.onlyIcon,
'x-component-props': {},
'x-reactions': [
{
dependencies: ['icon'],
fulfill: {
state: {
hidden: '{{!$deps[0]}}',
},
},
},
],
},
}, },
} as ISchema, } as ISchema,
onSubmit: ({ title, icon }) => { onSubmit: ({ title, icon, onlyIcon }) => {
fieldSchema.title = title; fieldSchema.title = title;
field.title = title; field.title = title;
field.componentProps.icon = icon; field.componentProps.icon = icon;
field.componentProps.onlyIcon = onlyIcon;
fieldSchema['x-component-props'] = fieldSchema['x-component-props'] || {}; fieldSchema['x-component-props'] = fieldSchema['x-component-props'] || {};
fieldSchema['x-component-props'].icon = icon; fieldSchema['x-component-props'].icon = icon;
fieldSchema['x-component-props'].onlyIcon = onlyIcon;
dn.emit('patch', { dn.emit('patch', {
schema: { schema: {
['x-uid']: fieldSchema['x-uid'], ['x-uid']: fieldSchema['x-uid'],

View File

@ -35,7 +35,7 @@ export const ActionLink: ComposedAction = withDynamicSchemaProps(
return ( return (
<Action <Action
{...props} {...props}
component={props.component || WrapperComponent} component={props.component || 'a'}
className={classnames('nb-action-link', props.className)} className={classnames('nb-action-link', props.className)}
isLink isLink
/> />

View File

@ -10,7 +10,7 @@
import { Field } from '@formily/core'; import { Field } from '@formily/core';
import { observer, Schema, useField, useFieldSchema, useForm } from '@formily/react'; import { observer, Schema, useField, useFieldSchema, useForm } from '@formily/react';
import { isPortalInBody } from '@nocobase/utils/client'; import { isPortalInBody } from '@nocobase/utils/client';
import { App, Button } from 'antd'; import { App, Button, Tooltip } from 'antd';
import classnames from 'classnames'; import classnames from 'classnames';
import debounce from 'lodash/debounce'; import debounce from 'lodash/debounce';
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
@ -369,6 +369,7 @@ Action.Popover = function ActionPopover(props) {
{props.children} {props.children}
</ErrorBoundary> </ErrorBoundary>
); );
return ( return (
<StablePopover <StablePopover
{...props} {...props}
@ -618,6 +619,22 @@ const RenderButtonInner = observer(
const actionTitle = typeof rawTitle === 'string' ? t(rawTitle, { ns: NAMESPACE_UI_SCHEMA }) : rawTitle; const actionTitle = typeof rawTitle === 'string' ? t(rawTitle, { ns: NAMESPACE_UI_SCHEMA }) : rawTitle;
const { opacity, ...restButtonStyle } = buttonStyle; const { opacity, ...restButtonStyle } = buttonStyle;
const linkStyle = isLink && opacity ? { opacity } : undefined; const linkStyle = isLink && opacity ? { opacity } : undefined;
const WrapperComponent = React.forwardRef(
({ component: Component = tarComponent || Button, icon, onlyIcon, children, ...restProps }: any, ref) => {
return (
<Component ref={ref} {...restProps}>
{onlyIcon ? (
<Tooltip title={restProps.title}>
<span style={{ marginRight: 3 }}>{icon && typeof icon === 'string' ? <Icon type={icon} /> : icon}</span>
</Tooltip>
) : (
<span style={{ marginRight: 3 }}>{icon && typeof icon === 'string' ? <Icon type={icon} /> : icon}</span>
)}
{onlyIcon ? children[1] : children}
</Component>
);
},
);
return ( return (
<SortableItem <SortableItem
role="button" role="button"
@ -630,10 +647,11 @@ const RenderButtonInner = observer(
disabled={disabled} disabled={disabled}
style={isLink ? restButtonStyle : buttonStyle} style={isLink ? restButtonStyle : buttonStyle}
onClick={process.env.__E2E__ ? handleButtonClick : debouncedClick} // E2E 中的点击操作都是很快的,如果加上 debounce 会导致 E2E 测试失败 onClick={process.env.__E2E__ ? handleButtonClick : debouncedClick} // E2E 中的点击操作都是很快的,如果加上 debounce 会导致 E2E 测试失败
component={tarComponent || Button} component={onlyIcon || tarComponent ? WrapperComponent : tarComponent || Button}
className={classnames(componentCls, hashId, className, 'nb-action')} className={classnames(componentCls, hashId, className, 'nb-action')}
type={type === 'danger' ? undefined : type} type={type === 'danger' ? undefined : type}
title={actionTitle} title={actionTitle}
onlyIcon={onlyIcon}
> >
{!onlyIcon && actionTitle && ( {!onlyIcon && actionTitle && (
<span className={icon ? 'nb-action-title' : null} style={linkStyle}> <span className={icon ? 'nb-action-title' : null} style={linkStyle}>

View File

@ -118,8 +118,5 @@ describe('Action.Popover', () => {
}); });
fireEvent.mouseLeave(btn); fireEvent.mouseLeave(btn);
await waitFor(() => {
expect(document.querySelector('.ant-popover')).not.toBeInTheDocument();
});
}); });
}); });

View File

@ -81,6 +81,7 @@ export interface ActionProps extends ButtonProps {
* @internal * @internal
*/ */
addChild?: boolean; addChild?: boolean;
onlyIcon?: boolean;
} }
export type ComposedAction = React.FC<ActionProps> & { export type ComposedAction = React.FC<ActionProps> & {

View File

@ -51,7 +51,7 @@ const InternalFilterAction = React.memo((props: FilterActionProps) => {
const form = useMemo<Form>(() => props.form || createForm(), []); const form = useMemo<Form>(() => props.form || createForm(), []);
// 新版 UISchema1.0 之后)中已经废弃了 useProps这里之所以继续保留是为了兼容旧版的 UISchema // 新版 UISchema1.0 之后)中已经废弃了 useProps这里之所以继续保留是为了兼容旧版的 UISchema
const { options, onSubmit, onReset, Container = StablePopover, icon } = useProps(props); const { options, onSubmit, onReset, Container = StablePopover, icon, onlyIcon } = useProps(props);
const onOpenChange = useCallback((visible: boolean): void => { const onOpenChange = useCallback((visible: boolean): void => {
setVisible(visible); setVisible(visible);
@ -77,7 +77,6 @@ const InternalFilterAction = React.memo((props: FilterActionProps) => {
/> />
); );
}, [field, fieldSchema, form, onReset, onSubmit, options]); }, [field, fieldSchema, form, onReset, onSubmit, options]);
return ( return (
<FilterActionContext.Provider value={filterActionContextValue}> <FilterActionContext.Provider value={filterActionContextValue}>
<Container <Container
@ -90,7 +89,7 @@ const InternalFilterAction = React.memo((props: FilterActionProps) => {
> >
{/* Adding a div here can prevent unnecessary re-rendering of Action */} {/* Adding a div here can prevent unnecessary re-rendering of Action */}
<div> <div>
<Action onClick={handleClick} icon={icon} /> <Action onClick={handleClick} icon={icon} onlyIcon={onlyIcon} />
</div> </div>
</Container> </Container>
</FilterActionContext.Provider> </FilterActionContext.Provider>

View File

@ -194,7 +194,8 @@ const useTableColumns = (
return css` return css`
.nb-action-link { .nb-action-link {
margin: -${token.paddingContentVerticalLG}px -${token.marginSM}px; margin: -${token.paddingContentVerticalLG}px -${token.marginSM}px;
padding: ${token.paddingContentVerticalLG}px ${token.paddingSM + 4}px; padding: ${token.paddingContentVerticalLG}px ${token.paddingContentVerticalLG}px ${token.paddingSM}px
${token.paddingSM}px;
} }
`; `;
}, [token.paddingContentVerticalLG, token.marginSM, token.margin]); }, [token.paddingContentVerticalLG, token.marginSM, token.margin]);