Merge branch 'master' into fix-bug-#51152

This commit is contained in:
zhengcp 2024-11-20 08:41:55 +08:00 committed by GitHub
commit 37cef7fe97
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
42 changed files with 488 additions and 184 deletions

View File

@ -8,9 +8,6 @@ import SiteContext from '../SiteContext';
import ContributorAvatar from './ContributorAvatar';
const useStyle = createStyles(({ token, css }) => ({
contributorsList: css`
margin-top: 120px !important;
`,
listMobile: css`
margin: 1em 0 !important;
`,
@ -50,7 +47,7 @@ const Contributors: React.FC<ContributorsProps> = ({ filename }) => {
}
return (
<div className={classNames(styles.contributorsList, { [styles.listMobile]: isMobile })}>
<div className={classNames({ [styles.listMobile]: isMobile })}>
<div className={styles.title}>{formatMessage({ id: 'app.content.contributors' })}</div>
<ContributorsList
cache

View File

@ -94,9 +94,11 @@ const Content: React.FC<React.PropsWithChildren> = ({ children }) => {
juejinLink={meta.frontmatter.juejin_url}
/>
</InViewSuspense>
<InViewSuspense fallback={<div style={{ height: 50, marginTop: 120 }} />}>
<Contributors filename={meta.frontmatter.filename} />
</InViewSuspense>
<div style={{ marginTop: 120 }}>
<InViewSuspense fallback={<div style={{ height: 50 }} />}>
<Contributors filename={meta.frontmatter.filename} />
</InViewSuspense>
</div>
</article>
<InViewSuspense fallback={null}>
<PrevAndNext rtl={isRTL} />

View File

@ -133,7 +133,7 @@ jobs:
bunx nyc report --reporter text -t coverage --report-dir coverage
rm -rf persist-coverage
- name: Upload coverage to codecov
uses: codecov/codecov-action@v4
uses: codecov/codecov-action@v5
with:
# use own token to upload coverage reports
token: ${{ secrets.CODECOV_TOKEN }}

View File

@ -58,7 +58,13 @@ jobs:
if (comment.body.includes('VISUAL_DIFF_FAILED')) {
hasDiffFailed = true;
}
if (comment.body.includes('- [x] Visual diff is acceptable')) {
// https://regex101.com/r/kLjudz/1
const RE = /(?<=\>\s\[!IMPORTANT\].*?- \[ \])/s;
if (
comment.body.includes('- [x] Visual diff is acceptable') &&
comment.body.match(RE) == null /** 检查 IMPORTANT 是否存在未勾选的 */
) {
hasMemberApprove = true;
}
}

View File

@ -62,5 +62,17 @@
"https://github.com/ant-design/ant-design/issues/50960",
"https://github.com/ant-design/ant-design/issues/50969"
],
"5.22.0": ["https://github.com/ant-design/ant-design/issues/51601"]
"5.21.6": [
"https://github.com/ant-design/ant-design/issues/51420",
"https://github.com/ant-design/ant-design/issues/51430"
],
"5.22.0": [
"https://github.com/ant-design/ant-design/issues/51601",
"https://github.com/ant-design/ant-design/issues/51420",
"https://github.com/ant-design/ant-design/issues/51430"
],
"5.22.1": [
"https://github.com/ant-design/ant-design/issues/51420",
"https://github.com/ant-design/ant-design/issues/51430"
]
}

View File

@ -15,6 +15,21 @@ tag: vVERSION
---
## 5.22.2
`2024-11-19`
- 🐞 Fix Input.OTP focus from advancing when previous input is empty. [#51664](https://github.com/ant-design/ant-design/pull/51664) [@thecodesalim](https://github.com/thecodesalim)
- 🐞 Adjust Modal function call not to scroll the confirm button when it get auto focused. [#51647](https://github.com/ant-design/ant-design/pull/51647) [@zombieJ](https://github.com/zombieJ)
- 🐞 Fix Form `rules` with same error content will cause React render warning. [#51636](https://github.com/ant-design/ant-design/pull/51636) [@zombieJ](https://github.com/zombieJ)
- 🐞 Refactor Button `focus` logic trigger with `useEffect` to resolve some async load case not get `autoFocus`. [#51624](https://github.com/ant-design/ant-design/pull/51624) [@zombieJ](https://github.com/zombieJ)
- 🐞 Fix Button custom icon not center-aligned. [#51652](https://github.com/ant-design/ant-design/pull/51652) [@afc163](https://github.com/afc163)
- 🐞 Fix Table `getCheckboxProps` event handlers being overridden by internal selection logic. [#51661](https://github.com/ant-design/ant-design/pull/51661) [@Zyf665](https://github.com/Zyf665)
- 🐞 Fix Tree that `onCheck` and `onSelect` were not properly triggered. [#51448](https://github.com/ant-design/ant-design/pull/51448) [@Wxh16144](https://github.com/Wxh16144)
- 🌐 Localization
- 🇷🇺 Add support for Russian translation. [#51619](https://github.com/ant-design/ant-design/pull/51619) [@avvakumovid](https://github.com/avvakumovid)
- 🇮🇹 Add support for Italian translation in TimePicker. [#51685](https://github.com/ant-design/ant-design/pull/51685) [@LorenzoCardinali](https://github.com/LorenzoCardinali)
## 5.22.1
`2024-11-13`

View File

@ -15,6 +15,21 @@ tag: vVERSION
---
## 5.22.2
`2024-11-19`
- 🐞 修复 Input.OTP 组件在有非法输入时仍会切换到下一个输入框的问题。[#51664](https://github.com/ant-design/ant-design/pull/51664) [@thecodesalim](https://github.com/thecodesalim)
- 🐞 调整 Modal 确认函数,使其在弹出后聚焦确认按钮时不要滚动窗体。[#51647](https://github.com/ant-design/ant-design/pull/51647) [@zombieJ](https://github.com/zombieJ)
- 🐞 修复 Form `rules` 生成多条相同错误时会报 React 渲染错误的问题。[#51636](https://github.com/ant-design/ant-design/pull/51636) [@zombieJ](https://github.com/zombieJ)
- 🐞 调整 Button 使用 `useEffect` 来触发 `autoFocus` 逻辑,以解决一些异步渲染场景下 Button 无法自动聚焦的问题。[#51624](https://github.com/ant-design/ant-design/pull/51624) [@zombieJ](https://github.com/zombieJ)
- 🐞 修复 Button 中使用自定义三方图标库时图标未居中的问题。[#51652](https://github.com/ant-design/ant-design/pull/51652) [@afc163](https://github.com/afc163)
- 🐞 修复 Table 组件 `getCheckboxProps` 中的事件处理器被内部选择逻辑覆盖的问题。[#51661](https://github.com/ant-design/ant-design/pull/51661) [@Zyf665](https://github.com/Zyf665)
- 🐞 修复 Tree 组件的 `onCheck``onSelect` 事件没有被正确触发的问题。[#51448](https://github.com/ant-design/ant-design/pull/51448) [@Wxh16144](https://github.com/Wxh16144)
- 🌐 本地化
- 🇷🇺 添加了俄语翻译支持。[#51619](https://github.com/ant-design/ant-design/pull/51619) [@avvakumovid](https://github.com/avvakumovid)
- 🇮🇹 为 TimePicker 添加了意大利语翻译。[#51685](https://github.com/ant-design/ant-design/pull/51685) [@LorenzoCardinali](https://github.com/LorenzoCardinali)
## 5.22.1
`2024-11-13`

View File

@ -52,7 +52,9 @@ const ActionButton: React.FC<ActionButtonProps> = (props) => {
let timeoutId: ReturnType<typeof setTimeout> | null = null;
if (autoFocus) {
timeoutId = setTimeout(() => {
buttonRef.current?.focus();
buttonRef.current?.focus({
preventScroll: true,
});
});
}
return () => {

View File

@ -474,4 +474,10 @@ describe('Button', () => {
'--ant-button-solid-text-color': '#000',
});
});
it('autoFocus should work', () => {
const { container } = render(<Button autoFocus>button</Button>);
expect(container.querySelector('button')).toBe(document.activeElement);
});
});

View File

@ -1,7 +1,7 @@
import React, { Children, createRef, useContext, useEffect, useMemo, useState } from 'react';
import React, { Children, useContext, useEffect, useMemo, useRef, useState } from 'react';
import classNames from 'classnames';
import omit from 'rc-util/lib/omit';
import { composeRef } from 'rc-util/lib/ref';
import { useComposeRef } from 'rc-util/lib/ref';
import { devUseWarning } from '../_util/warning';
import Wave from '../_util/wave';
@ -22,7 +22,7 @@ import { isTwoCNChar, isUnBorderedButtonVariant, spaceChildren } from './buttonH
import IconWrapper from './IconWrapper';
import LoadingIcon from './LoadingIcon';
import useStyle from './style';
import CompactCmp from './style/compactCmp';
import Compact from './style/compact';
export type LegacyButtonType = ButtonType | 'danger';
@ -119,6 +119,7 @@ const InternalCompoundedButton = React.forwardRef<
classNames: customClassNames,
style: customStyle = {},
autoInsertSpace,
autoFocus,
...rest
} = props;
@ -162,13 +163,15 @@ const InternalCompoundedButton = React.forwardRef<
const [hasTwoCNChar, setHasTwoCNChar] = useState<boolean>(false);
const internalRef = createRef<HTMLButtonElement | HTMLAnchorElement>();
const buttonRef = useRef<HTMLButtonElement | HTMLAnchorElement>();
const buttonRef = composeRef(ref, internalRef);
const mergedRef = useComposeRef(ref, buttonRef);
const needInserted =
Children.count(children) === 1 && !icon && !isUnBorderedButtonVariant(mergedVariant);
// ========================= Effect =========================
// Loading
useEffect(() => {
let delayTimer: ReturnType<typeof setTimeout> | null = null;
if (loadingOrDelay.delay > 0) {
@ -190,12 +193,13 @@ const InternalCompoundedButton = React.forwardRef<
return cleanupTimer;
}, [loadingOrDelay]);
// Two chinese characters check
useEffect(() => {
// FIXME: for HOC usage like <FormatMessage />
if (!buttonRef || !(buttonRef as any).current || !mergedInsertSpace) {
if (!buttonRef.current || !mergedInsertSpace) {
return;
}
const buttonText = (buttonRef as any).current.textContent;
const buttonText = buttonRef.current.textContent || '';
if (needInserted && isTwoCNChar(buttonText)) {
if (!hasTwoCNChar) {
setHasTwoCNChar(true);
@ -203,8 +207,16 @@ const InternalCompoundedButton = React.forwardRef<
} else if (hasTwoCNChar) {
setHasTwoCNChar(false);
}
}, [buttonRef]);
});
// Auto focus
useEffect(() => {
if (autoFocus && buttonRef.current) {
buttonRef.current.focus();
}
}, []);
// ========================= Events =========================
const handleClick = React.useCallback(
(e: React.MouseEvent<HTMLButtonElement | HTMLAnchorElement, MouseEvent>) => {
// FIXME: https://github.com/ant-design/ant-design/issues/30207
@ -217,6 +229,7 @@ const InternalCompoundedButton = React.forwardRef<
[props.onClick, innerLoading, mergedDisabled],
);
// ========================== Warn ==========================
if (process.env.NODE_ENV !== 'production') {
const warning = devUseWarning('Button');
@ -233,6 +246,7 @@ const InternalCompoundedButton = React.forwardRef<
);
}
// ========================== Size ==========================
const { compactSize, compactItemClassnames } = useCompactItemContext(prefixCls, direction);
const sizeClassNameMap = { large: 'lg', small: 'sm', middle: undefined };
@ -245,6 +259,7 @@ const InternalCompoundedButton = React.forwardRef<
const linkButtonRestProps = omit(rest as ButtonProps & { navigate: any }, ['navigate']);
// ========================= Render =========================
const classes = classNames(
prefixCls,
hashId,
@ -301,7 +316,7 @@ const InternalCompoundedButton = React.forwardRef<
href={mergedDisabled ? undefined : linkButtonRestProps.href}
style={fullStyle}
onClick={handleClick}
ref={buttonRef as React.Ref<HTMLAnchorElement>}
ref={mergedRef as React.Ref<HTMLAnchorElement>}
tabIndex={mergedDisabled ? -1 : 0}
>
{iconNode}
@ -318,12 +333,11 @@ const InternalCompoundedButton = React.forwardRef<
style={fullStyle}
onClick={handleClick}
disabled={mergedDisabled}
ref={buttonRef as React.Ref<HTMLButtonElement>}
ref={mergedRef as React.Ref<HTMLButtonElement>}
>
{iconNode}
{kids}
{/* Styles: compact */}
{!!compactItemClassnames && <CompactCmp key="compact" prefixCls={prefixCls} />}
{compactItemClassnames && <Compact prefixCls={prefixCls} />}
</button>
);

View File

@ -0,0 +1,50 @@
// Style as inline component
import type { CSSObject } from '@ant-design/cssinjs';
import { genCompactItemStyle } from '../../style/compact-item';
import { genCompactItemVerticalStyle } from '../../style/compact-item-vertical';
import type { GenerateStyle } from '../../theme/internal';
import { genSubStyleComponent } from '../../theme/internal';
import type { ButtonToken } from './token';
import { prepareComponentToken, prepareToken } from './token';
const genButtonCompactStyle: GenerateStyle<ButtonToken> = (token) => {
const { componentCls, colorPrimaryHover, lineWidth, calc } = token;
const insetOffset = calc(lineWidth).mul(-1).equal();
const getCompactBorderStyle = (vertical?: boolean) =>
({
[`${componentCls}-compact${vertical ? '-vertical' : ''}-item${componentCls}-primary:not([disabled])`]:
{
'& + &::before': {
position: 'absolute',
top: vertical ? insetOffset : 0,
insetInlineStart: vertical ? 0 : insetOffset,
backgroundColor: colorPrimaryHover,
content: '""',
width: vertical ? '100%' : lineWidth,
height: vertical ? lineWidth : '100%',
},
},
}) as CSSObject;
// Special styles for Primary Button
return {
...getCompactBorderStyle(),
...getCompactBorderStyle(true),
};
};
// ============================== Export ==============================
export default genSubStyleComponent(
['Button', 'compact'],
(token) => {
const buttonToken = prepareToken(token);
return [
// Space Compact
genCompactItemStyle(buttonToken),
genCompactItemVerticalStyle(buttonToken),
genButtonCompactStyle(buttonToken),
];
},
prepareComponentToken,
);

View File

@ -1,72 +0,0 @@
// Style as inline component
import { unit } from '@ant-design/cssinjs';
import { genCompactItemStyle } from '../../style/compact-item';
import { genCompactItemVerticalStyle } from '../../style/compact-item-vertical';
import type { GenerateStyle } from '../../theme/internal';
import { genSubStyleComponent } from '../../theme/internal';
import type { ButtonToken } from './token';
import { prepareComponentToken, prepareToken } from './token';
const genButtonCompactStyle: GenerateStyle<ButtonToken> = (token) => {
const { componentCls, calc } = token;
return {
[componentCls]: {
// Special styles for Primary Button
[`&-compact-item${componentCls}-primary`]: {
[`&:not([disabled]) + ${componentCls}-compact-item${componentCls}-primary:not([disabled])`]:
{
position: 'relative',
'&:before': {
position: 'absolute',
top: calc(token.lineWidth).mul(-1).equal(),
insetInlineStart: calc(token.lineWidth).mul(-1).equal(),
display: 'inline-block',
width: token.lineWidth,
height: `calc(100% + ${unit(token.lineWidth)} * 2)`,
backgroundColor: token.colorPrimaryHover,
content: '""',
},
},
},
// Special styles for Primary Button
'&-compact-vertical-item': {
[`&${componentCls}-primary`]: {
[`&:not([disabled]) + ${componentCls}-compact-vertical-item${componentCls}-primary:not([disabled])`]:
{
position: 'relative',
'&:before': {
position: 'absolute',
top: calc(token.lineWidth).mul(-1).equal(),
insetInlineStart: calc(token.lineWidth).mul(-1).equal(),
display: 'inline-block',
width: `calc(100% + ${unit(token.lineWidth)} * 2)`,
height: token.lineWidth,
backgroundColor: token.colorPrimaryHover,
content: '""',
},
},
},
},
},
};
};
// ============================== Export ==============================
export default genSubStyleComponent(
['Button', 'compact'],
(token) => {
const buttonToken = prepareToken(token);
return [
// Space Compact
genCompactItemStyle(buttonToken),
genCompactItemVerticalStyle(buttonToken),
genButtonCompactStyle(buttonToken),
];
},
prepareComponentToken,
);

View File

@ -38,12 +38,8 @@ const genSharedButtonStyle: GenerateStyle<ButtonToken, CSSObject> = (token): CSS
pointerEvents: 'none',
},
'> span': {
display: 'inline-block',
},
[`${componentCls}-icon`]: {
lineHeight: 1,
[`> span, ${componentCls}-icon`]: {
display: 'inline-flex',
},
'> a': {
@ -529,10 +525,11 @@ const genButtonStyle = (token: ButtonToken, prefixCls = ''): CSSInterpolation =>
buttonPaddingHorizontal,
iconCls,
buttonPaddingVertical,
motionDurationSlow,
motionEaseInOut,
buttonIconOnlyFontSize,
opacityLoading,
} = token;
const iconOnlyCls = `${componentCls}-icon-only`;
return [
{
[prefixCls]: {
@ -542,7 +539,7 @@ const genButtonStyle = (token: ButtonToken, prefixCls = ''): CSSInterpolation =>
padding: `${unit(buttonPaddingVertical!)} ${unit(buttonPaddingHorizontal!)}`,
borderRadius,
[`&${iconOnlyCls}`]: {
[`&${componentCls}-icon-only`]: {
width: controlHeight,
paddingInline: 0,
@ -556,22 +553,21 @@ const genButtonStyle = (token: ButtonToken, prefixCls = ''): CSSInterpolation =>
},
[iconCls]: {
fontSize: token.buttonIconOnlyFontSize,
fontSize: buttonIconOnlyFontSize,
},
},
// Loading
[`&${componentCls}-loading`]: {
opacity: token.opacityLoading,
opacity: opacityLoading,
cursor: 'default',
},
[`${componentCls}-loading-icon`]: {
transition: `width ${token.motionDurationSlow} ${token.motionEaseInOut}, opacity ${token.motionDurationSlow} ${token.motionEaseInOut}`,
transition: `width ${motionDurationSlow} ${motionEaseInOut}, opacity ${motionDurationSlow} ${motionEaseInOut}`,
},
},
},
// Shape - patch prefixCls again to override solid border radius style
{
[`${componentCls}${componentCls}-circle${prefixCls}`]: genCircleButtonStyle(token),

View File

@ -31,7 +31,7 @@ demo:
<code src="./demo/search.tsx">Search</code>
<code src="./demo/lazy.tsx">Load Options Lazily</code>
<code src="./demo/fields-name.tsx">Custom Field Names</code>
<code src="./demo/suffix.tsx" debug>Prefix and Suffix</code>
<code src="./demo/suffix.tsx" version="5.22.0">Prefix and Suffix</code>
<code src="./demo/custom-dropdown.tsx">Custom dropdown</code>
<code src="./demo/placement.tsx">Placement</code>
<code src="./demo/status.tsx">Status</code>
@ -67,7 +67,7 @@ Common props ref[Common props](/docs/react/common-props)
| maxTagCount | Max tag count to show. `responsive` will cost render performance | number \| `responsive` | - | 4.17.0 |
| maxTagPlaceholder | Placeholder for not showing tags | ReactNode \| function(omittedValues) | - | 4.17.0 |
| maxTagTextLength | Max tag text length to show | number | - | 4.17.0 |
| notFoundContent | Specify content to show when no result matches | string | `Not Found` | |
| notFoundContent | Specify content to show when no result matches | ReactNode | `Not Found` | |
| open | Set visible of cascader popup | boolean | - | 4.17.0 |
| options | The data options of cascade | [Option](#option)\[] | - | |
| placeholder | The input placeholder | string | - | |

View File

@ -32,7 +32,7 @@ demo:
<code src="./demo/search.tsx">搜索</code>
<code src="./demo/lazy.tsx">动态加载选项</code>
<code src="./demo/fields-name.tsx">自定义字段名</code>
<code src="./demo/suffix.tsx" debug>前后缀</code>
<code src="./demo/suffix.tsx" version="5.22.0">前后缀</code>
<code src="./demo/custom-dropdown.tsx">扩展菜单</code>
<code src="./demo/placement.tsx">弹出位置</code>
<code src="./demo/status.tsx">自定义状态</code>
@ -68,7 +68,7 @@ demo:
| maxTagCount | 最多显示多少个 tag响应式模式会对性能产生损耗 | number \| `responsive` | - | 4.17.0 |
| maxTagPlaceholder | 隐藏 tag 时显示的内容 | ReactNode \| function(omittedValues) | - | 4.17.0 |
| maxTagTextLength | 最大显示的 tag 文本长度 | number | - | 4.17.0 |
| notFoundContent | 当下拉列表为空时显示的内容 | string | `Not Found` | |
| notFoundContent | 当下拉列表为空时显示的内容 | ReactNode | `Not Found` | |
| open | 控制浮层显隐 | boolean | - | 4.17.0 |
| options | 可选项数据源 | [Option](#option)\[] | - | |
| placeholder | 输入框占位文本 | string | - | |

View File

@ -78,6 +78,17 @@ const ErrorList: React.FC<ErrorListProps> = ({
];
}, [help, helpStatus, debounceErrors, debounceWarnings]);
const filledKeyFullKeyList = React.useMemo<ErrorEntity[]>(() => {
const keysCount: Record<string, number> = {};
fullKeyList.forEach(({ key }) => {
keysCount[key] = (keysCount[key] || 0) + 1;
});
return fullKeyList.map((entity, index) => ({
...entity,
key: keysCount[entity.key] > 1 ? `${entity.key}-fallback-${index}` : entity.key,
}));
}, [fullKeyList]);
const helpProps: { id?: string } = {};
if (fieldId) {
@ -88,7 +99,7 @@ const ErrorList: React.FC<ErrorListProps> = ({
<CSSMotion
motionDeadline={collapseMotion.motionDeadline}
motionName={`${prefixCls}-show-help`}
visible={!!fullKeyList.length}
visible={!!filledKeyFullKeyList.length}
onVisibleChanged={onVisibleChanged}
>
{(holderProps) => {
@ -109,7 +120,7 @@ const ErrorList: React.FC<ErrorListProps> = ({
role="alert"
>
<CSSMotionList
keys={fullKeyList}
keys={filledKeyFullKeyList}
{...initCollapseMotion(prefixCls)}
motionName={`${prefixCls}-show-help-item`}
component={false}

View File

@ -2459,4 +2459,60 @@ describe('Form', () => {
fireEvent.click(container.querySelector('input')!);
expect(container.querySelector('input')?.checked).toBeTruthy();
});
it('not warning for react key', async () => {
const MockInput = (props: { onChange?: (value: number[]) => void }) => (
<Input
onChange={({ target: { value } }) => {
props.onChange?.(value.split(',').map(Number));
}}
/>
);
const { container } = render(
<Form>
<Form.Item>
<Form.Item
name="test"
rules={[
{
type: 'array',
defaultField: {
type: 'number',
min: 10,
message: 'LESS_THAN_10',
},
},
]}
>
<MockInput />
</Form.Item>
</Form.Item>
</Form>,
);
function expectErrors(errors: string[]) {
expect(container.querySelectorAll('.ant-form-item-explain-error')).toHaveLength(
errors.length,
);
errors.forEach((error, index) => {
expect(container.querySelectorAll('.ant-form-item-explain-error')[index]).toHaveTextContent(
error,
);
});
}
// user type something and clear
await changeValue(0, '1');
expectErrors(['LESS_THAN_10']);
await changeValue(0, '1,1');
expectErrors(['LESS_THAN_10', 'LESS_THAN_10']);
await changeValue(0, '1');
expectErrors(['LESS_THAN_10']);
await changeValue(0, '100');
expectErrors([]);
});
});

View File

@ -208,7 +208,7 @@ const OTP = React.forwardRef<OTPRef, OTPProps>((props, ref) => {
const nextCells = patchValue(index, txt);
const nextIndex = Math.min(index + txt.length, length - 1);
if (nextIndex !== index) {
if (nextIndex !== index && nextCells[index] !== undefined) {
refs.current[nextIndex]?.focus();
}

View File

@ -1,10 +1,8 @@
import * as React from 'react';
import { forwardRef } from 'react';
import classNames from 'classnames';
import type { TextAreaRef as RcTextAreaRef } from 'rc-textarea';
import type { TextAreaRef as RcTextAreaRef, TextAreaProps as RcTextAreaProps } from 'rc-textarea';
import RcTextArea from 'rc-textarea';
import type { TextAreaProps as RcTextAreaProps } from 'rc-textarea/lib/interface';
import getAllowClear from '../_util/getAllowClear';
import type { InputStatus } from '../_util/statusUtils';
import { getMergedStatus, getStatusClassNames } from '../_util/statusUtils';

View File

@ -25,17 +25,17 @@ const ContainerHeight = 400;
const App: React.FC = () => {
const [data, setData] = useState<UserItem[]>([]);
const appendData = () => {
const appendData = (showMessage = true) => {
fetch(fakeDataUrl)
.then((res) => res.json())
.then((body) => {
setData(data.concat(body.results));
message.success(`${body.results.length} more items loaded!`);
showMessage && message.success(`${body.results.length} more items loaded!`);
});
};
useEffect(() => {
appendData();
appendData(false);
}, []);
const onScroll = (e: React.UIEvent<HTMLElement, UIEvent>) => {

View File

@ -80,6 +80,7 @@ const localeValues: Locale = {
copy: 'Копировать',
copied: 'Скопировано',
expand: 'Раскрыть',
collapse: "Свернуть",
},
Form: {
optional: '(необязательно)',
@ -138,6 +139,12 @@ const localeValues: Locale = {
expired: 'QR-код устарел',
refresh: 'Обновить',
},
ColorPicker: {
presetEmpty: 'Пустой',
transparent: 'Прозрачный',
singleColor: 'Один цвет',
gradientColor: 'Градиент',
}
};
export default localeValues;

View File

@ -52,7 +52,7 @@ return (
<code src="./demo/label-in-value.tsx">Get value of selected item</code>
<code src="./demo/automatic-tokenization.tsx">Automatic tokenization</code>
<code src="./demo/select-users.tsx">Search and Select Users</code>
<code src="./demo/suffix.tsx" debug>Prefix and Suffix</code>
<code src="./demo/suffix.tsx" version="5.22.0">Prefix and Suffix</code>
<code src="./demo/custom-dropdown-menu.tsx">Custom dropdown</code>
<code src="./demo/hide-selected.tsx">Hide Already Selected</code>
<code src="./demo/variant.tsx" version="5.13.0">Variants</code>

View File

@ -53,7 +53,7 @@ return (
<code src="./demo/label-in-value.tsx">获得选项的文本</code>
<code src="./demo/automatic-tokenization.tsx">自动分词</code>
<code src="./demo/select-users.tsx">搜索用户</code>
<code src="./demo/suffix.tsx" debug>前后缀</code>
<code src="./demo/suffix.tsx" version="5.22.0">前后缀</code>
<code src="./demo/custom-dropdown-menu.tsx">扩展菜单</code>
<code src="./demo/hide-selected.tsx">隐藏已选择选项</code>
<code src="./demo/variant.tsx" version="5.13.0">多种形态</code>

View File

@ -1905,4 +1905,76 @@ describe('Table.rowSelection', () => {
);
});
});
it('should trigger both custom and internal checkbox events', () => {
const onClickMock = jest.fn();
const onChangeMock = jest.fn();
const getCheckboxProps = () => ({
onClick: onClickMock,
onChange: onChangeMock,
});
const { container } = render(
<Table
rowSelection={{
type: 'checkbox',
getCheckboxProps,
}}
columns={columns}
dataSource={data}
/>,
);
const firstRowCheckbox = container.querySelector('tbody tr:first-child input[type="checkbox"]');
expect(firstRowCheckbox).toBeTruthy();
fireEvent.click(firstRowCheckbox!);
expect(onClickMock).toHaveBeenCalled();
expect(onClickMock.mock.calls.length).toBe(1);
expect(onChangeMock).toHaveBeenCalled();
expect(onChangeMock.mock.calls.length).toBe(1);
const changeEvent = onChangeMock.mock.calls[0][0];
expect(changeEvent).toHaveProperty('target');
expect(changeEvent.target).toHaveProperty('checked');
});
it('should trigger both custom and internal radio events', () => {
const onClickMock = jest.fn();
const onChangeMock = jest.fn();
const getCheckboxProps = () => ({
onClick: onClickMock,
onChange: onChangeMock,
});
const { container } = render(
<Table
rowSelection={{
type: 'radio',
getCheckboxProps,
}}
columns={columns}
dataSource={data}
/>,
);
const firstRowRadio = container.querySelector('tbody tr:first-child input[type="radio"]');
expect(firstRowRadio).toBeTruthy();
fireEvent.click(firstRowRadio!);
expect(onClickMock).toHaveBeenCalled();
expect(onClickMock.mock.calls.length).toBe(1);
expect(onChangeMock).toHaveBeenCalled();
expect(onChangeMock.mock.calls.length).toBe(1);
const changeEvent = onChangeMock.mock.calls[0][0];
expect(changeEvent).toHaveProperty('target');
expect(changeEvent.target).toHaveProperty('checked');
});
});

View File

@ -496,17 +496,21 @@ const useSelection = <RecordType extends AnyObject = AnyObject>(
renderCell = (_, record, index) => {
const key = getRowKey(record, index);
const checked = keySet.has(key);
const checkboxProps = checkboxPropsMap.get(key);
return {
node: (
<Radio
{...checkboxPropsMap.get(key)}
{...checkboxProps}
checked={checked}
onClick={(e) => e.stopPropagation()}
onClick={(e) => {
e.stopPropagation();
checkboxProps?.onClick?.(e);
}}
onChange={(event) => {
if (!keySet.has(key)) {
triggerSingleSelection(key, true, [key], event.nativeEvent);
}
checkboxProps?.onChange?.(event);
}}
/>
),
@ -538,8 +542,12 @@ const useSelection = <RecordType extends AnyObject = AnyObject>(
indeterminate={mergedIndeterminate}
checked={checked}
skipGroup
onClick={(e) => e.stopPropagation()}
onChange={({ nativeEvent }) => {
onClick={(e) => {
e.stopPropagation();
checkboxProps?.onClick?.(e);
}}
onChange={(event) => {
const { nativeEvent } = event;
const { shiftKey } = nativeEvent;
const currentSelectedIndex = recordKeys.findIndex((item) => item === key);
const isMultiple = derivedSelectedKeys.some((item) => recordKeys.includes(item));
@ -595,6 +603,7 @@ const useSelection = <RecordType extends AnyObject = AnyObject>(
} else {
updatePrevSelectedIndex(currentSelectedIndex);
}
checkboxProps?.onChange?.(event);
}}
/>
),

View File

@ -2,6 +2,7 @@ import type { TimePickerLocale } from '../index';
const locale: TimePickerLocale = {
placeholder: "Selezionare l'orario",
rangePlaceholder: ['Inizio orario', 'Fine orario'],
};
export default locale;

View File

@ -52,13 +52,13 @@ Common props ref[Common props](/docs/react/common-props)
### Timeline
| Property | Description | Type | Default |
| --- | --- | --- | --- |
| mode | By sending `alternate` the timeline will distribute the nodes to the left and right | `left` \| `alternate` \| `right` | - |
| pending | Set the last ghost node's existence or its content | ReactNode | false |
| pendingDot | Set the dot of the last ghost node when pending is true | ReactNode | &lt;LoadingOutlined /&gt; |
| reverse | Whether reverse nodes or not | boolean | false |
| items | Each node of timeline | [Items](#Items)[] | 5.2.0 |
| Property | Description | Type | Default | Version |
| --- | --- | --- | --- | --- |
| mode | By sending `alternate` the timeline will distribute the nodes to the left and right | `left` \| `alternate` \| `right` | - | |
| pending | Set the last ghost node's existence or its content | ReactNode | false | |
| pendingDot | Set the dot of the last ghost node when pending is true | ReactNode | &lt;LoadingOutlined /&gt; | |
| reverse | Whether reverse nodes or not | boolean | false | |
| items | Each node of timeline | [Items](#Items)[] | - | 5.2.0 |
### Items

View File

@ -53,13 +53,13 @@ return (
### Timeline
| 参数 | 说明 | 类型 | 默认值 |
| --- | --- | --- | --- |
| mode | 通过设置 `mode` 可以改变时间轴和内容的相对位置 | `left` \| `alternate` \| `right` | - |
| pending | 指定最后一个幽灵节点是否存在或内容 | ReactNode | false |
| pendingDot | 当最后一个幽灵节点存在時,指定其时间图点 | ReactNode | &lt;LoadingOutlined /&gt; |
| reverse | 节点排序 | boolean | false |
| items | 选项配置 | [Items](#Items)[] | 5.2.0 |
| 参数 | 说明 | 类型 | 默认值 | 版本 |
| --- | --- | --- | --- | --- |
| mode | 通过设置 `mode` 可以改变时间轴和内容的相对位置 | `left` \| `alternate` \| `right` | - | |
| pending | 指定最后一个幽灵节点是否存在或内容 | ReactNode | false | |
| pendingDot | 当最后一个幽灵节点存在時,指定其时间图点 | ReactNode | &lt;LoadingOutlined /&gt; | |
| reverse | 节点排序 | boolean | false | |
| items | 选项配置 | [Items](#Items)[] | - | 5.2.0 |
### Items

View File

@ -24,7 +24,7 @@ demo:
<code src="./demo/treeLine.tsx">Show Tree Line</code>
<code src="./demo/placement.tsx">Placement</code>
<code src="./demo/status.tsx">Status</code>
<code src="./demo/suffix.tsx" debug>Prefix and Suffix</code>
<code src="./demo/suffix.tsx" version="5.22.0">Prefix and Suffix</code>
<code src="./demo/render-panel.tsx" debug>_InternalPanelDoNotUseOrYouWillBeFired</code>
<code src="./demo/component-token.tsx" debug>Component Token</code>

View File

@ -25,7 +25,7 @@ demo:
<code src="./demo/treeLine.tsx">线性样式</code>
<code src="./demo/placement.tsx">弹出位置</code>
<code src="./demo/status.tsx">自定义状态</code>
<code src="./demo/suffix.tsx" debug>前后缀</code>
<code src="./demo/suffix.tsx" version="5.22.0">前后缀</code>
<code src="./demo/render-panel.tsx" debug>\_InternalPanelDoNotUseOrYouWillBeFired</code>
<code src="./demo/component-token.tsx" debug>组件 Token</code>

View File

@ -1,3 +1,5 @@
import { extendTest } from '../../../tests/shared/demoTest';
extendTest('tree', { skip: ['big-data.tsx', 'virtual-scroll.tsx', 'component-token.tsx'] });
extendTest('tree', {
skip: ['big-data.tsx', 'virtual-scroll.tsx', 'component-token.tsx', 'directory-debug.tsx'],
});

View File

@ -1,3 +1,5 @@
import demoTest from '../../../tests/shared/demoTest';
demoTest('tree', { skip: ['big-data.tsx', 'virtual-scroll.tsx', 'component-token.tsx'] });
demoTest('tree', {
skip: ['big-data.tsx', 'virtual-scroll.tsx', 'component-token.tsx', 'directory-debug.tsx'],
});

View File

@ -0,0 +1,7 @@
## zh-CN
调试 [#51210](https://github.com/ant-design/ant-design/pull/51210), [#51448](https://github.com/ant-design/ant-design/pull/51448#issuecomment-2449144872)
## en-US
Debugging [#51210](https://github.com/ant-design/ant-design/pull/51210), [#51448](https://github.com/ant-design/ant-design/pull/51448#issuecomment-2449144872)

View File

@ -0,0 +1,65 @@
import React from 'react';
import { Flex, Tree } from 'antd';
import type { GetProps, TreeDataNode } from 'antd';
const { DirectoryTree } = Tree;
const treeData: TreeDataNode[] = [
{
title: 'parent 0',
key: '0-0',
children: [
{ title: 'leaf 0-0', key: '0-0-0', isLeaf: true },
{ title: 'leaf 0-1', key: '0-0-1', isLeaf: true },
],
},
{
title: 'parent 1',
key: '0-1',
children: [
{ title: 'leaf 1-0', key: '0-1-0', isLeaf: true },
{ title: 'leaf 1-1', key: '0-1-1', isLeaf: true },
],
},
];
const sharedProps: GetProps<typeof DirectoryTree> = {
treeData,
defaultExpandAll: true,
onSelect: (keys, info) => {
console.log('Trigger Select', keys, info);
},
onExpand: (keys, info) => {
console.log('Trigger Expand', keys, info);
},
};
const DemoOne = () => <DirectoryTree draggable defaultSelectedKeys={['0-0-0']} />;
const DemoTwo = () => <DirectoryTree {...sharedProps} checkable defaultSelectedKeys={['0-1-0']} />;
const DemoThree = () => (
<DirectoryTree {...sharedProps} draggable checkable defaultSelectedKeys={['0-1']} />
);
const BasicDemo = () => <DirectoryTree {...sharedProps} multiple treeData={treeData} />;
const NormalDemo = () => <Tree {...sharedProps} defaultSelectedKeys={['0-1']} />;
const NormalCheckDemo = () => <Tree {...sharedProps} checkable defaultSelectedKeys={['0-1']} />;
const NormalDragDemo = () => <Tree {...sharedProps} draggable defaultSelectedKeys={['0-1-0']} />;
const App = () => (
<Flex wrap gap="large">
<DemoOne />
<DemoTwo />
<DemoThree />
<BasicDemo />
<NormalDemo />
<NormalCheckDemo />
<NormalDragDemo />
</Flex>
);
export default App;

View File

@ -24,6 +24,7 @@ Almost anything can be represented in a tree structure. Examples include directo
<code src="./demo/line.tsx">Tree with line</code>
<code src="./demo/customized-icon.tsx">Customize Icon</code>
<code src="./demo/directory.tsx">directory</code>
<code src="./demo/directory-debug.tsx" debug>Directory Debug</code>
<code src="./demo/switcher-icon.tsx">Customize collapse/expand icon</code>
<code src="./demo/virtual-scroll.tsx">Virtual scroll</code>
<code src="./demo/drag-debug.tsx" debug>Drag Debug</code>

View File

@ -25,6 +25,7 @@ demo:
<code src="./demo/line.tsx">连接线</code>
<code src="./demo/customized-icon.tsx">自定义图标</code>
<code src="./demo/directory.tsx">目录</code>
<code src="./demo/directory-debug.tsx" debug>目录 Debug</code>
<code src="./demo/switcher-icon.tsx">自定义展开/折叠图标</code>
<code src="./demo/virtual-scroll.tsx">虚拟滚动</code>
<code src="./demo/drag-debug.tsx" debug>Drag Debug</code>

View File

@ -38,15 +38,14 @@ export const genDirectoryStyle = ({
},
},
[`${treeCls}-switcher`]: {
marginInlineEnd: 0,
[`${treeCls}-switcher, ${treeCls}-checkbox, ${treeCls}-draggable-icon`]: {
zIndex: 1,
},
// ============= Selected =============
'&-selected': {
[`${treeCls}-switcher, ${treeCls}-draggable-icon`]: {
color: directoryNodeSelectedColor,
zIndex: 1,
},
// >>> Title

View File

@ -120,7 +120,6 @@ export const genBaseStyle = (prefixCls: string, token: TreeToken): CSSObject =>
nodeHoverBg,
colorTextQuaternary,
} = token;
const treeCheckBoxMarginHorizontal = token.marginXXS;
return {
[treeCls]: {
@ -253,6 +252,14 @@ export const genBaseStyle = (prefixCls: string, token: TreeToken): CSSObject =>
visibility: 'hidden',
},
// Switcher / Checkbox
[`${treeCls}-switcher, ${treeCls}-checkbox`]: {
marginInlineEnd: token
.calc(token.calc(titleHeight).sub(token.controlInteractiveSize))
.div(2)
.equal(),
},
// >>> Switcher
[`${treeCls}-switcher`]: {
...getSwitchStyle(prefixCls, token),
@ -260,15 +267,10 @@ export const genBaseStyle = (prefixCls: string, token: TreeToken): CSSObject =>
flex: 'none',
alignSelf: 'stretch',
width: titleHeight,
margin: 0,
textAlign: 'center',
cursor: 'pointer',
userSelect: 'none',
transition: `all ${token.motionDurationSlow}`,
marginInlineEnd: token
.calc(token.calc(titleHeight).sub(token.controlInteractiveSize))
.div(2)
.equal(),
'&-noop': {
cursor: 'unset',
@ -329,14 +331,6 @@ export const genBaseStyle = (prefixCls: string, token: TreeToken): CSSObject =>
},
},
// >>> Checkbox
[`${treeCls}-checkbox`]: {
top: 'initial',
marginInlineEnd: treeCheckBoxMarginHorizontal,
alignSelf: 'flex-start',
marginTop: token.marginXXS,
},
// >>> Title
// add `${treeCls}-checkbox + span` to cover checkbox `${checkboxCls} + span`
[`${treeCls}-node-content-wrapper`]: {

View File

@ -5,11 +5,10 @@ import { clearFix, textEllipsis } from '../../style';
import type { GenerateStyle } from '../../theme/internal';
const genListStyle: GenerateStyle<UploadToken> = (token) => {
const { componentCls, antCls, iconCls, fontSize, lineHeight, calc } = token;
const { componentCls, iconCls, fontSize, lineHeight, calc } = token;
const itemCls = `${componentCls}-list-item`;
const actionsCls = `${itemCls}-actions`;
const actionCls = `${itemCls}-action`;
const listItemHeightSM = token.fontHeightSM;
return {
[`${componentCls}-wrapper`]: {
@ -25,6 +24,7 @@ const genListStyle: GenerateStyle<UploadToken> = (token) => {
display: 'flex',
alignItems: 'center',
transition: `background-color ${token.motionDurationSlow}`,
borderRadius: token.borderRadiusSM,
'&:hover': {
backgroundColor: token.controlItemBgHover,
@ -56,12 +56,6 @@ const genListStyle: GenerateStyle<UploadToken> = (token) => {
`]: {
opacity: 1,
},
[`${actionCls}${antCls}-btn`]: {
height: listItemHeightSM,
border: 0,
lineHeight: 1,
},
},
[`${componentCls}-icon ${iconCls}`]: {

View File

@ -1,6 +1,6 @@
{
"name": "antd",
"version": "5.22.1",
"version": "5.22.2",
"description": "An enterprise-class UI design language and React components implementation",
"license": "MIT",
"funding": {
@ -126,7 +126,7 @@
"rc-dialog": "~9.6.0",
"rc-drawer": "~7.2.0",
"rc-dropdown": "~4.2.0",
"rc-field-form": "~2.5.0",
"rc-field-form": "~2.5.1",
"rc-image": "~7.11.0",
"rc-input": "~1.6.3",
"rc-input-number": "~9.3.0",
@ -135,7 +135,7 @@
"rc-motion": "^2.9.3",
"rc-notification": "~5.6.2",
"rc-pagination": "~4.3.0",
"rc-picker": "~4.8.0",
"rc-picker": "~4.8.1",
"rc-progress": "~4.0.0",
"rc-rate": "~2.13.0",
"rc-resize-observer": "^1.4.0",

View File

@ -12,6 +12,7 @@ import pixelmatch from 'pixelmatch';
import { PNG } from 'pngjs';
import sharp from 'sharp';
import simpleGit from 'simple-git';
import filter from 'lodash/filter';
import markdown2Html from './convert';
@ -283,9 +284,17 @@ ${fullReport}
let diffCount = 0;
// Summary
const badCount = badCases.length;
const commentReportLimit = isLocalEnv ? badCount : 8;
const changedCount = filter(badCases, { type: 'changed' }).length;
const removedCount = filter(badCases, { type: 'removed' }).length;
const addedCount = filter(badCases, { type: 'added' }).length;
for (const badCase of badCases) {
diffCount += 1;
if (diffCount <= 10) {
if (diffCount <= commentReportLimit) {
// 将图片下方增加文件名
reportMdStr += generateLineReport(badCase, publicPath, currentRef, true);
}
@ -293,18 +302,37 @@ ${fullReport}
fullVersionMd += generateLineReport(badCase, publicPath, currentRef, false);
}
reportMdStr += `\n\nCheck <a href="${htmlReportLink}" target="_blank">Full Report</a> for details`;
const hasMore = badCount > commentReportLimit;
if (hasMore) {
reportMdStr += [
'\r',
'> [!WARNING]',
`> There are more diffs not shown in the table. Please check the <a href="${htmlReportLink}" target="_blank">Full Report</a> for details.`,
'\r',
].join('\n');
}
// tips for comment `Pass Visual Diff` will pass the CI
if (!passed) {
reportMdStr += `
const summaryLine = [
changedCount > 0 && `🔄 **${changedCount}** changed`,
removedCount > 0 && `🛑 **${removedCount}** removed`,
addedCount > 0 && `🆕 **${addedCount}** added`,
]
.filter(Boolean)
.join(', ');
-----
If you think the visual diff is acceptable, please check:
- [ ] Visual diff is acceptable
`;
reportMdStr += [
'\n---\n',
'> [!IMPORTANT]',
`> There are **${badCount}** diffs found in this PR: ${summaryLine}.`,
'> **Please check all items:**',
hasMore && '> - [ ] Checked all diffs in the full report',
'> - [ ] Visual diff is acceptable',
]
.filter(Boolean)
.join('\n');
}
// convert fullVersionMd to html

View File

@ -29,6 +29,11 @@
border-collapse: collapse;
}
table > thead {
position: sticky;
top: 0;
}
th,
td {
padding: 8px;
@ -61,13 +66,22 @@
border: 0;
}
table > thead {
position: unset;
}
th,
td {
width: 100%;
display: block;
padding: 6px;
border: none;
}
td:last-child {
border-bottom: 3px dashed darkgray;
}
th {
text-align: left;
background-color: transparent;