choer: merge feature into next

This commit is contained in:
lijianan
2025-03-16 07:35:04 +08:00
18 changed files with 436 additions and 157 deletions

View File

@@ -64,7 +64,6 @@ const useThemeAnimation = () => {
event: React.MouseEvent<HTMLElement, MouseEvent>,
isDark: boolean,
) => {
// @ts-ignore
if (!(event && typeof document.startViewTransition === 'function')) {
return;
}
@@ -85,13 +84,10 @@ const useThemeAnimation = () => {
'color-scheme',
);
document
// @ts-ignore
.startViewTransition(async () => {
// wait for theme change end
while (colorBgElevated === animateRef.current.colorBgElevated) {
await new Promise((resolve) => {
setTimeout(resolve, 1000 / 60);
});
await new Promise<void>((resolve) => setTimeout(resolve, 1000 / 60));
}
const root = document.documentElement;
root.classList.remove(isDark ? 'dark' : 'light');
@@ -111,7 +107,6 @@ const useThemeAnimation = () => {
// inject transition style
useEffect(() => {
// @ts-ignore
if (typeof document.startViewTransition === 'function') {
updateCSS(viewTransitionStyle, 'view-transition-style');
}

View File

@@ -0,0 +1,20 @@
import type { ReactNode } from 'react';
import { isValidElement } from 'react';
import type { TooltipProps } from '../tooltip';
function convertToTooltipProps<P extends TooltipProps>(tooltip: P | ReactNode): P | null {
// isNil
if (tooltip === undefined || tooltip === null) {
return null;
}
if (typeof tooltip === 'object' && !isValidElement(tooltip)) {
return tooltip as P;
}
return {
title: tooltip,
} as P;
}
export default convertToTooltipProps;

View File

@@ -1,7 +1,8 @@
import React, { useContext, useMemo } from 'react';
import React from 'react';
import omit from '@rc-component/util/lib/omit';
import classNames from 'classnames';
import convertToTooltipProps from '../_util/convertToTooltipProps';
import { useZIndex } from '../_util/hooks/useZIndex';
import { devUseWarning } from '../_util/warning';
import Badge from '../badge';
@@ -13,12 +14,7 @@ import type BackTop from './BackTop';
import FloatButtonGroupContext from './context';
import Content from './FloatButtonContent';
import type FloatButtonGroup from './FloatButtonGroup';
import type {
FloatButtonContentProps,
FloatButtonElement,
FloatButtonProps,
FloatButtonShape,
} from './interface';
import type { FloatButtonElement, FloatButtonProps, FloatButtonShape } from './interface';
import type PurePanel from './PurePanel';
import useStyle from './style';
@@ -39,8 +35,8 @@ const InternalFloatButton = React.forwardRef<FloatButtonElement, FloatButtonProp
badge = {},
...restProps
} = props;
const { getPrefixCls, direction } = useContext<ConfigConsumerProps>(ConfigContext);
const groupShape = useContext<FloatButtonShape | undefined>(FloatButtonGroupContext);
const { getPrefixCls, direction } = React.useContext<ConfigConsumerProps>(ConfigContext);
const groupShape = React.useContext<FloatButtonShape | undefined>(FloatButtonGroupContext);
const prefixCls = getPrefixCls(floatButtonPrefixCls, customizePrefixCls);
const rootCls = useCSSVarCls(prefixCls);
const [hashId, cssVarCls] = useStyle(prefixCls, rootCls);
@@ -69,14 +65,9 @@ const InternalFloatButton = React.forwardRef<FloatButtonElement, FloatButtonProp
// 虽然在 ts 中已经 omit 过了,但是为了防止多余的属性被透传进来,这里再 omit 一遍,以防万一
const badgeProps = omit(badge, ['title', 'children', 'status', 'text'] as any[]);
const contentProps = useMemo<FloatButtonContentProps>(
() => ({ prefixCls, description, icon, type }),
[prefixCls, description, icon, type],
);
let buttonNode = (
<div className={`${prefixCls}-body`}>
<Content {...contentProps} />
<Content prefixCls={prefixCls} description={description} icon={icon} />
</div>
);
@@ -84,12 +75,10 @@ const InternalFloatButton = React.forwardRef<FloatButtonElement, FloatButtonProp
buttonNode = <Badge {...badgeProps}>{buttonNode}</Badge>;
}
if ('tooltip' in props) {
buttonNode = (
<Tooltip title={tooltip} placement={direction === 'rtl' ? 'right' : 'left'}>
{buttonNode}
</Tooltip>
);
// ============================ Tooltip ============================
const tooltipProps = convertToTooltipProps(tooltip);
if (tooltipProps) {
buttonNode = <Tooltip {...tooltipProps}>{buttonNode}</Tooltip>;
}
if (process.env.NODE_ENV !== 'production') {
@@ -102,16 +91,14 @@ const InternalFloatButton = React.forwardRef<FloatButtonElement, FloatButtonProp
);
}
return (
props.href ? (
<a ref={ref} {...restProps} className={classString} style={mergedStyle}>
{buttonNode}
</a>
) : (
<button ref={ref} {...restProps} className={classString} style={mergedStyle} type={htmlType}>
{buttonNode}
</button>
)
return props.href ? (
<a ref={ref} {...restProps} className={classString} style={mergedStyle}>
{buttonNode}
</a>
) : (
<button ref={ref} {...restProps} className={classString} style={mergedStyle} type={htmlType}>
{buttonNode}
</button>
);
});

View File

@@ -5,20 +5,16 @@ import classNames from 'classnames';
import type { FloatButtonContentProps } from './interface';
const FloatButtonContent: React.FC<FloatButtonContentProps> = (props) => {
const { icon, description, prefixCls, className } = props;
const { icon, description, prefixCls, className, ...rest } = props;
const defaultElement = (
<div className={`${prefixCls}-icon`}>
<FileTextOutlined />
</div>
);
return (
<div
onClick={props.onClick}
onFocus={props.onFocus}
onMouseEnter={props.onMouseEnter}
onMouseLeave={props.onMouseLeave}
className={classNames(className, `${prefixCls}-content`)}
>
<div {...rest} className={classNames(className, `${prefixCls}-content`)}>
{icon || description ? (
<>
{icon && <div className={`${prefixCls}-icon`}>{icon}</div>}

View File

@@ -140,12 +140,12 @@ Array [
</sup>
</span>
<div
class="ant-tooltip ant-zoom-big-fast-appear ant-zoom-big-fast-appear-prepare ant-zoom-big-fast css-var-test-id ant-tooltip-placement-left"
class="ant-tooltip ant-zoom-big-fast-appear ant-zoom-big-fast-appear-prepare ant-zoom-big-fast css-var-test-id ant-tooltip-placement-top"
style="--arrow-x: 0px; --arrow-y: 0px; left: -1000vw; top: -1000vh; box-sizing: border-box;"
>
<div
class="ant-tooltip-arrow"
style="position: absolute; top: 0px; right: 0px;"
style="position: absolute; bottom: 0px; left: 0px;"
/>
<div
class="ant-tooltip-content"
@@ -2038,65 +2038,125 @@ Array [
exports[`renders components/float-button/demo/shape.tsx extend context correctly 2`] = `[]`;
exports[`renders components/float-button/demo/tooltip.tsx extend context correctly 1`] = `
<button
class="css-var-test-id ant-float-btn-css-var ant-float-btn ant-float-btn-default ant-float-btn-circle"
type="button"
>
<div
aria-describedby="test-id"
class="ant-float-btn-body"
Array [
<button
class="css-var-test-id ant-float-btn-css-var ant-float-btn ant-float-btn-default ant-float-btn-circle"
style="inset-block-end: 108px;"
type="button"
>
<div
class="ant-float-btn-content"
aria-describedby="test-id"
class="ant-float-btn-body"
>
<div
class="ant-float-btn-icon"
class="ant-float-btn-content"
>
<span
aria-label="file-text"
class="anticon anticon-file-text"
role="img"
<div
class="ant-float-btn-icon"
>
<svg
aria-hidden="true"
data-icon="file-text"
fill="currentColor"
focusable="false"
height="1em"
viewBox="64 64 896 896"
width="1em"
<span
aria-label="file-text"
class="anticon anticon-file-text"
role="img"
>
<path
d="M854.6 288.6L639.4 73.4c-6-6-14.1-9.4-22.6-9.4H192c-17.7 0-32 14.3-32 32v832c0 17.7 14.3 32 32 32h640c17.7 0 32-14.3 32-32V311.3c0-8.5-3.4-16.7-9.4-22.7zM790.2 326H602V137.8L790.2 326zm1.8 562H232V136h302v216a42 42 0 0042 42h216v494zM504 618H320c-4.4 0-8 3.6-8 8v48c0 4.4 3.6 8 8 8h184c4.4 0 8-3.6 8-8v-48c0-4.4-3.6-8-8-8zM312 490v48c0 4.4 3.6 8 8 8h384c4.4 0 8-3.6 8-8v-48c0-4.4-3.6-8-8-8H320c-4.4 0-8 3.6-8 8z"
/>
</svg>
</span>
</div>
</div>
</div>
<div
class="ant-tooltip ant-zoom-big-fast-appear ant-zoom-big-fast-appear-prepare ant-zoom-big-fast css-var-test-id ant-tooltip-placement-left"
style="--arrow-x: 0px; --arrow-y: 0px; left: -1000vw; top: -1000vh; box-sizing: border-box;"
>
<div
class="ant-tooltip-arrow"
style="position: absolute; top: 0px; right: 0px;"
/>
<div
class="ant-tooltip-content"
>
<div
class="ant-tooltip-inner"
id="test-id"
role="tooltip"
>
<div>
Documents
<svg
aria-hidden="true"
data-icon="file-text"
fill="currentColor"
focusable="false"
height="1em"
viewBox="64 64 896 896"
width="1em"
>
<path
d="M854.6 288.6L639.4 73.4c-6-6-14.1-9.4-22.6-9.4H192c-17.7 0-32 14.3-32 32v832c0 17.7 14.3 32 32 32h640c17.7 0 32-14.3 32-32V311.3c0-8.5-3.4-16.7-9.4-22.7zM790.2 326H602V137.8L790.2 326zm1.8 562H232V136h302v216a42 42 0 0042 42h216v494zM504 618H320c-4.4 0-8 3.6-8 8v48c0 4.4 3.6 8 8 8h184c4.4 0 8-3.6 8-8v-48c0-4.4-3.6-8-8-8zM312 490v48c0 4.4 3.6 8 8 8h384c4.4 0 8-3.6 8-8v-48c0-4.4-3.6-8-8-8H320c-4.4 0-8 3.6-8 8z"
/>
</svg>
</span>
</div>
</div>
</div>
</div>
</button>
<div
class="ant-tooltip ant-zoom-big-fast-appear ant-zoom-big-fast-appear-prepare ant-zoom-big-fast ant-tooltip-blue css-var-test-id ant-tooltip-placement-top"
style="--arrow-x: 0px; --arrow-y: 0px; left: -1000vw; top: -1000vh; box-sizing: border-box;"
>
<div
class="ant-tooltip-arrow"
style="position: absolute; bottom: 0px; left: 0px;"
/>
<div
class="ant-tooltip-content"
>
<div
class="ant-tooltip-inner"
id="test-id"
role="tooltip"
>
Since 5.25.0+
</div>
</div>
</div>
</button>,
<button
class="css-var-test-id ant-float-btn-css-var ant-float-btn ant-float-btn-default ant-float-btn-circle"
type="button"
>
<div
aria-describedby="test-id"
class="ant-float-btn-body"
>
<div
class="ant-float-btn-content"
>
<div
class="ant-float-btn-icon"
>
<span
aria-label="file-text"
class="anticon anticon-file-text"
role="img"
>
<svg
aria-hidden="true"
data-icon="file-text"
fill="currentColor"
focusable="false"
height="1em"
viewBox="64 64 896 896"
width="1em"
>
<path
d="M854.6 288.6L639.4 73.4c-6-6-14.1-9.4-22.6-9.4H192c-17.7 0-32 14.3-32 32v832c0 17.7 14.3 32 32 32h640c17.7 0 32-14.3 32-32V311.3c0-8.5-3.4-16.7-9.4-22.7zM790.2 326H602V137.8L790.2 326zm1.8 562H232V136h302v216a42 42 0 0042 42h216v494zM504 618H320c-4.4 0-8 3.6-8 8v48c0 4.4 3.6 8 8 8h184c4.4 0 8-3.6 8-8v-48c0-4.4-3.6-8-8-8zM312 490v48c0 4.4 3.6 8 8 8h384c4.4 0 8-3.6 8-8v-48c0-4.4-3.6-8-8-8H320c-4.4 0-8 3.6-8 8z"
/>
</svg>
</span>
</div>
</div>
</div>
<div
class="ant-tooltip ant-zoom-big-fast-appear ant-zoom-big-fast-appear-prepare ant-zoom-big-fast css-var-test-id ant-tooltip-placement-top"
style="--arrow-x: 0px; --arrow-y: 0px; left: -1000vw; top: -1000vh; box-sizing: border-box;"
>
<div
class="ant-tooltip-arrow"
style="position: absolute; bottom: 0px; left: 0px;"
/>
<div
class="ant-tooltip-content"
>
<div
class="ant-tooltip-inner"
id="test-id"
role="tooltip"
>
<div>
Documents
</div>
</div>
</div>
</div>
</button>,
]
`;
exports[`renders components/float-button/demo/tooltip.tsx extend context correctly 2`] = `[]`;

View File

@@ -1974,43 +1974,83 @@ Array [
`;
exports[`renders components/float-button/demo/tooltip.tsx correctly 1`] = `
<button
class="css-var-test-id ant-float-btn-css-var ant-float-btn ant-float-btn-default ant-float-btn-circle"
type="button"
>
<div
aria-describedby="test-id"
class="ant-float-btn-body"
Array [
<button
class="css-var-test-id ant-float-btn-css-var ant-float-btn ant-float-btn-default ant-float-btn-circle"
style="inset-block-end:108px"
type="button"
>
<div
class="ant-float-btn-content"
aria-describedby="test-id"
class="ant-float-btn-body"
>
<div
class="ant-float-btn-icon"
class="ant-float-btn-content"
>
<span
aria-label="file-text"
class="anticon anticon-file-text"
role="img"
<div
class="ant-float-btn-icon"
>
<svg
aria-hidden="true"
data-icon="file-text"
fill="currentColor"
focusable="false"
height="1em"
viewBox="64 64 896 896"
width="1em"
<span
aria-label="file-text"
class="anticon anticon-file-text"
role="img"
>
<path
d="M854.6 288.6L639.4 73.4c-6-6-14.1-9.4-22.6-9.4H192c-17.7 0-32 14.3-32 32v832c0 17.7 14.3 32 32 32h640c17.7 0 32-14.3 32-32V311.3c0-8.5-3.4-16.7-9.4-22.7zM790.2 326H602V137.8L790.2 326zm1.8 562H232V136h302v216a42 42 0 0042 42h216v494zM504 618H320c-4.4 0-8 3.6-8 8v48c0 4.4 3.6 8 8 8h184c4.4 0 8-3.6 8-8v-48c0-4.4-3.6-8-8-8zM312 490v48c0 4.4 3.6 8 8 8h384c4.4 0 8-3.6 8-8v-48c0-4.4-3.6-8-8-8H320c-4.4 0-8 3.6-8 8z"
/>
</svg>
</span>
<svg
aria-hidden="true"
data-icon="file-text"
fill="currentColor"
focusable="false"
height="1em"
viewBox="64 64 896 896"
width="1em"
>
<path
d="M854.6 288.6L639.4 73.4c-6-6-14.1-9.4-22.6-9.4H192c-17.7 0-32 14.3-32 32v832c0 17.7 14.3 32 32 32h640c17.7 0 32-14.3 32-32V311.3c0-8.5-3.4-16.7-9.4-22.7zM790.2 326H602V137.8L790.2 326zm1.8 562H232V136h302v216a42 42 0 0042 42h216v494zM504 618H320c-4.4 0-8 3.6-8 8v48c0 4.4 3.6 8 8 8h184c4.4 0 8-3.6 8-8v-48c0-4.4-3.6-8-8-8zM312 490v48c0 4.4 3.6 8 8 8h384c4.4 0 8-3.6 8-8v-48c0-4.4-3.6-8-8-8H320c-4.4 0-8 3.6-8 8z"
/>
</svg>
</span>
</div>
</div>
</div>
</div>
</button>
</button>,
<button
class="css-var-test-id ant-float-btn-css-var ant-float-btn ant-float-btn-default ant-float-btn-circle"
type="button"
>
<div
aria-describedby="test-id"
class="ant-float-btn-body"
>
<div
class="ant-float-btn-content"
>
<div
class="ant-float-btn-icon"
>
<span
aria-label="file-text"
class="anticon anticon-file-text"
role="img"
>
<svg
aria-hidden="true"
data-icon="file-text"
fill="currentColor"
focusable="false"
height="1em"
viewBox="64 64 896 896"
width="1em"
>
<path
d="M854.6 288.6L639.4 73.4c-6-6-14.1-9.4-22.6-9.4H192c-17.7 0-32 14.3-32 32v832c0 17.7 14.3 32 32 32h640c17.7 0 32-14.3 32-32V311.3c0-8.5-3.4-16.7-9.4-22.7zM790.2 326H602V137.8L790.2 326zm1.8 562H232V136h302v216a42 42 0 0042 42h216v494zM504 618H320c-4.4 0-8 3.6-8 8v48c0 4.4 3.6 8 8 8h184c4.4 0 8-3.6 8-8v-48c0-4.4-3.6-8-8-8zM312 490v48c0 4.4 3.6 8 8 8h384c4.4 0 8-3.6 8-8v-48c0-4.4-3.6-8-8-8H320c-4.4 0-8 3.6-8 8z"
/>
</svg>
</span>
</div>
</div>
</div>
</button>,
]
`;
exports[`renders components/float-button/demo/type.tsx correctly 1`] = `

View File

@@ -1,5 +1,5 @@
import { imageDemoTest } from '../../../tests/shared/imageTest';
describe('float-button image', () => {
imageDemoTest('float-button', { onlyViewport: ['back-top.tsx'] });
imageDemoTest('float-button', { onlyViewport: true });
});

View File

@@ -63,15 +63,27 @@ describe('FloatButton', () => {
errSpy.mockRestore();
});
it('tooltip should support number `0`', async () => {
jest.useFakeTimers();
const { container } = render(<FloatButton tooltip={0} />);
fireEvent.mouseEnter(container.querySelector<HTMLDivElement>('.ant-float-btn-body')!);
await waitFakeTimer();
const element = container.querySelector('.ant-tooltip')?.querySelector('.ant-tooltip-inner');
expect(element?.textContent).toBe('0');
jest.clearAllTimers();
jest.useRealTimers();
describe('tooltip', () => {
it('tooltip should support number `0`', async () => {
jest.useFakeTimers();
const { container } = render(<FloatButton tooltip={0} />);
fireEvent.mouseEnter(container.querySelector<HTMLDivElement>('.ant-float-btn-body')!);
await waitFakeTimer();
const element = container.querySelector('.ant-tooltip')?.querySelector('.ant-tooltip-inner');
expect(element?.textContent).toBe('0');
jest.clearAllTimers();
jest.useRealTimers();
});
it('tooltip should support tooltipProps', async () => {
jest.useFakeTimers();
const { container } = render(<FloatButton tooltip={{ title: 'hi' }} />);
fireEvent.mouseEnter(container.querySelector<HTMLDivElement>('.ant-float-btn-body')!);
await waitFakeTimer();
const element = container.querySelector('.ant-tooltip')?.querySelector('.ant-tooltip-inner');
expect(element?.textContent).toBe('hi');
jest.clearAllTimers();
jest.useRealTimers();
});
});
it('getOffset should return 0 when radius is 0', () => {

View File

@@ -1,6 +1,19 @@
import React from 'react';
import { FloatButton } from 'antd';
const App: React.FC = () => <FloatButton tooltip={<div>Documents</div>} />;
const App: React.FC = () => (
<>
<FloatButton
style={{ insetBlockEnd: 108 }}
tooltip={{
// tooltipProps is supported starting from version 5.25.0.
title: 'Since 5.25.0+',
color: 'blue',
placement: 'top',
}}
/>
<FloatButton tooltip={<div>Documents</div>} />
</>
);
export default App;

View File

@@ -44,7 +44,7 @@ Common props ref[Common props](/docs/react/common-props)
| --- | --- | --- | --- | --- |
| icon | Set the icon component of button | ReactNode | - | |
| description | Text and other | ReactNode | - | |
| tooltip | The text shown in the tooltip | ReactNode \| () => ReactNode | | |
| tooltip | The text shown in the tooltip | ReactNode \| [TooltipProps](/components/tooltip#api) | - | TooltipProps: 5.25.0 |
| type | Setting button type | `default` \| `primary` | `default` | |
| shape | Setting button shape | `circle` \| `square` | `circle` | |
| onClick | Set the handler to handle `click` event | (event) => void | - | |

View File

@@ -45,7 +45,7 @@ tag: 5.0.0
| --- | --- | --- | --- | --- |
| icon | 自定义图标 | ReactNode | - | |
| description | 文字及其它内容 | ReactNode | - | |
| tooltip | 气泡卡片的内容 | ReactNode \| () => ReactNode | - | |
| tooltip | 气泡卡片的内容 | ReactNode \| [TooltipProps](/components/tooltip-cn#api) | - | TooltipProps: 5.25.0 |
| type | 设置按钮类型 | `default` \| `primary` | `default` | |
| shape | 设置按钮形状 | `circle` \| `square` | `circle` | |
| onClick | 点击按钮时的回调 | (event) => void | - | |

View File

@@ -27,7 +27,7 @@ export interface FloatButtonProps extends React.DOMAttributes<FloatButtonElement
description?: React.ReactNode;
type?: FloatButtonType;
shape?: FloatButtonShape;
tooltip?: TooltipProps['title'];
tooltip?: React.ReactNode | TooltipProps;
href?: string;
target?: React.HTMLAttributeAnchorTarget;
badge?: FloatButtonBadgeProps;

View File

@@ -2,6 +2,7 @@ import * as React from 'react';
import QuestionCircleOutlined from '@ant-design/icons/QuestionCircleOutlined';
import classNames from 'classnames';
import convertToTooltipProps from '../_util/convertToTooltipProps';
import type { ColProps } from '../grid/col';
import Col from '../grid/col';
import { useLocale } from '../locale';
@@ -19,20 +20,6 @@ export type WrapperTooltipProps = TooltipProps & {
export type LabelTooltipType = WrapperTooltipProps | React.ReactNode;
function toTooltipProps(tooltip: LabelTooltipType): WrapperTooltipProps | null {
if (!tooltip) {
return null;
}
if (typeof tooltip === 'object' && !React.isValidElement(tooltip)) {
return tooltip as WrapperTooltipProps;
}
return {
title: tooltip,
};
}
export interface FormItemLabelProps {
colon?: boolean;
htmlFor?: string;
@@ -98,7 +85,7 @@ const FormItemLabel: React.FC<FormItemLabelProps & { required?: boolean; prefixC
}
// Tooltip
const tooltipProps = toTooltipProps(tooltip);
const tooltipProps = convertToTooltipProps(tooltip);
if (tooltipProps) {
const { icon = <QuestionCircleOutlined />, ...restTooltipProps } = tooltipProps;

View File

@@ -20,6 +20,7 @@ import type { SizeType } from '../config-provider/SizeContext';
import { FormItemInputContext } from '../form/context';
import useVariant from '../form/hooks/useVariants';
import { useCompactItemContext } from '../space/Compact';
import useHandleResizeWrapper from './hooks/useHandleResizeWrapper';
import type { InputFocusOptions } from './Input';
import { triggerFocus } from './Input';
import { useSharedStyle } from './style';
@@ -58,6 +59,7 @@ const TextArea = forwardRef<TextAreaRef, TextAreaProps>((props, ref) => {
style,
styles,
variant: customVariant,
showCount,
...rest
} = props;
@@ -117,6 +119,8 @@ const TextArea = forwardRef<TextAreaRef, TextAreaProps>((props, ref) => {
const mergedAllowClear = getAllowClear(allowClear ?? contextAllowClear);
const { handleResizeWrapper } = useHandleResizeWrapper();
return (
<RcTextArea
autoComplete={contextAutoComplete}
@@ -165,6 +169,10 @@ const TextArea = forwardRef<TextAreaRef, TextAreaProps>((props, ref) => {
prefixCls={prefixCls}
suffix={hasFeedback && <span className={`${prefixCls}-textarea-suffix`}>{feedbackIcon}</span>}
ref={innerRef}
onResize={(size) => {
rest.onResize?.(size);
showCount && handleResizeWrapper(innerRef.current);
}}
/>
);
});

View File

@@ -9,11 +9,14 @@ import {
fireEvent,
pureRender,
render,
renderHook,
triggerResize,
waitFakeTimer,
waitFakeTimer19,
} from '../../../tests/utils';
import type { TextAreaRef } from '../TextArea';
import useHandleResizeWrapper from '../hooks/useHandleResizeWrapper';
import type { TextAreaRef as RcTextAreaRef } from 'rc-textarea';
const { TextArea } = Input;
@@ -531,3 +534,112 @@ describe('TextArea allowClear', () => {
errSpy.mockRestore();
});
});
describe('TextArea useHandleResizeWrapper', () => {
let requestAnimationFrameSpy: jest.SpyInstance;
beforeAll(() => {
// Use fake timers to control requestAnimationFrame.
jest.useFakeTimers();
// Override requestAnimationFrame to simulate a 16ms delay.
requestAnimationFrameSpy = jest
.spyOn(window, 'requestAnimationFrame')
.mockImplementation((cb: FrameRequestCallback) => {
return window.setTimeout(() => cb(performance.now()), 16);
});
});
afterAll(() => {
jest.useRealTimers();
requestAnimationFrameSpy.mockRestore();
});
it('does nothing when rcTextArea is null', () => {
const { result } = renderHook(() => useHandleResizeWrapper());
// Calling with null should not throw or change anything.
expect(() => result.current?.handleResizeWrapper(null)).not.toThrow();
});
it('does nothing when style width does not include "px"', () => {
const { result } = renderHook(() => useHandleResizeWrapper());
const fakeRcTextArea = {
resizableTextArea: {
textArea: {
style: {
width: '100', // missing 'px'
},
},
},
nativeElement: {
offsetWidth: 110,
style: {} as any,
},
} as unknown as RcTextAreaRef;
result.current?.handleResizeWrapper(fakeRcTextArea);
// Fast-forward time to see if any scheduled callback would execute.
jest.advanceTimersByTime(16);
// nativeElement.style.width remains unchanged.
expect(fakeRcTextArea.nativeElement.style.width).toBeUndefined();
});
it('adjusts width correctly when offsetWidth is slightly greater than the textArea width (increased scenario)', () => {
const { result } = renderHook(() => useHandleResizeWrapper());
const fakeRcTextArea = {
resizableTextArea: {
textArea: {
style: {
width: '100px', // valid width with px
},
},
},
// offsetWidth is 101 so the difference is 1 (< ELEMENT_GAP of 2)
nativeElement: {
offsetWidth: 101,
style: {} as any,
},
} as unknown as RcTextAreaRef;
// Immediately after calling handleResizeWrapper, the update is scheduled.
expect(fakeRcTextArea.nativeElement.style.width).toBeUndefined();
result.current?.handleResizeWrapper(fakeRcTextArea);
// Fast-forward time to trigger the requestAnimationFrame callback.
jest.advanceTimersByTime(16);
// Expected new width: 100 + 2 = 102px.
expect(fakeRcTextArea.nativeElement.style.width).toBe('102px');
});
it('adjusts width correctly when offsetWidth is significantly greater than the textArea width (decreased scenario)', () => {
const { result } = renderHook(() => useHandleResizeWrapper());
const fakeRcTextArea = {
resizableTextArea: {
textArea: {
style: {
width: '100px',
},
},
},
// offsetWidth is 105 so the difference is 5 (> ELEMENT_GAP of 2)
nativeElement: {
offsetWidth: 105,
style: {} as any,
},
} as unknown as RcTextAreaRef;
// Immediately after calling handleResizeWrapper, the update is scheduled.
expect(fakeRcTextArea.nativeElement.style.width).toBeUndefined();
result.current?.handleResizeWrapper(fakeRcTextArea);
// Fast-forward time to trigger the requestAnimationFrame callback.
jest.advanceTimersByTime(16);
// Expected new width remains: 100 + 2 = 102px.
expect(fakeRcTextArea.nativeElement.style.width).toBe('102px');
});
});

View File

@@ -0,0 +1,40 @@
import { TextAreaRef } from 'rc-textarea';
import React from 'react';
type ResizeWrapperHandler = (rcTextArea: TextAreaRef | null) => void;
const ELEMENT_GAP = 2;
const adjustElementWidth = (width: number, wrapper: HTMLElement): void => {
if (wrapper.offsetWidth - width < ELEMENT_GAP) {
// The textarea's width is increased
wrapper.style.width = `${width + ELEMENT_GAP}px`;
} else if (wrapper.offsetWidth - width > ELEMENT_GAP) {
// The textarea's width is decreased
wrapper.style.width = `${width + ELEMENT_GAP}px`;
}
};
let isScheduled = false;
const requestAnimationFrameDecorator = (callback: () => void) => {
if (!isScheduled) {
isScheduled = true;
requestAnimationFrame(() => {
callback();
isScheduled = false;
});
}
};
export default function useHandleResizeWrapper(): { handleResizeWrapper: ResizeWrapperHandler } {
const handleResizeWrapper: ResizeWrapperHandler = React.useCallback((rcTextArea) => {
if (!rcTextArea) return;
if (rcTextArea.resizableTextArea.textArea.style.width.includes('px')) {
const width = parseInt(rcTextArea.resizableTextArea.textArea.style.width.replace('px', ''));
requestAnimationFrameDecorator(() => adjustElementWidth(width, rcTextArea.nativeElement));
}
}, []);
return { handleResizeWrapper };
}

View File

@@ -2,14 +2,12 @@ import type { UploadProps } from '../interface';
export const successRequest: UploadProps['customRequest'] = ({ onSuccess, file }) => {
setTimeout(() => {
// @ts-ignore
onSuccess?.(null, file);
});
};
export const errorRequest: UploadProps['customRequest'] = ({ onError }) => {
setTimeout(() => {
// @ts-ignore
onError?.();
onError?.(new Error('test error'));
});
};

View File

@@ -89,6 +89,7 @@
"Artin",
"Arvin Xu",
"Ash Kumar",
"Ashcon Partovi",
"Ashot Mnatsakanyan",
"Austaras",
"Avan",
@@ -198,6 +199,7 @@
"Daewoong Moon",
"Dalton Craven",
"Damian Green",
"Dan M.",
"Dan Minshew",
"Dana Janoskova",
"Dane David",
@@ -349,6 +351,7 @@
"HJin.me",
"Hai Phan Nguyen",
"Haibin Yu",
"Hakan Kosdag",
"Hale Deng",
"Han Han",
"Hanai",
@@ -383,6 +386,7 @@
"Humble",
"Hyunseok.Kim",
"ILdar Nogmanov",
"Ideveloper (이승규)",
"Igor",
"Igor Andriushchenko",
"Igor G",
@@ -699,6 +703,7 @@
"Muhammad Abuzar",
"Muhammad Sameer",
"Muhammad Sohaib Raza",
"Muhammad Taif Khan",
"MuxinFeng",
"MyeongHyun Lew",
"Mykyta Velykanov",
@@ -987,11 +992,13 @@
"Vu Hoang Minh",
"Vyacheslav Kamenev",
"Vyacheslav Sedykh",
"Waiter",
"Walter Barbagallo",
"Wang Jun",
"Wang Riwu",
"Wang Zhengchen",
"Wang yb",
"Wangye",
"Wanpan",
"Warren Seymour",
"WeLong",
@@ -1121,6 +1128,7 @@
"appleshell",
"arange",
"arifemrecelik",
"arron.laihongwei",
"arturpfb",
"ascodelife",
"ascoders",
@@ -1398,6 +1406,7 @@
"netcon",
"neverland",
"ngolin",
"nicholas-codecov",
"nick-ChenZe",
"niko",
"nitinknolder",
@@ -1594,6 +1603,7 @@
"zhoulixiang",
"zhuguibiao",
"zhujun24",
"zhuzhu_coder",
"zhyupe",
"zilong",
"zinkey",
@@ -1607,12 +1617,12 @@
"ztplz",
"zty",
"zuiidea",
"zx6658",
"zxyao",
"zytjs",
"zz",
"°))))彡",
"Ömer Faruk APLAK",
"Đào Văn Hùng",
"Ștefan Filip",
"Зухриддин Камильжанов",
"रोहन मल्होत्रा",
@@ -1644,6 +1654,7 @@
"只捱宅",
"可乐",
"叶枫",
"合木",
"吕立青",
"吴泽康",
"啸生",