mirror of
https://github.com/ant-design/ant-design.git
synced 2025-06-07 09:26:06 +08:00
Merge pull request #48189 from ant-design/feature
chore: merge feature into master
This commit is contained in:
commit
5384a97f37
@ -6,9 +6,9 @@ import type { UseClosableParams } from '../hooks/useClosable';
|
||||
import useClosable from '../hooks/useClosable';
|
||||
|
||||
type ParamsOfUseClosable = [
|
||||
UseClosableParams['closable'],
|
||||
UseClosableParams['closeIcon'],
|
||||
UseClosableParams['defaultClosable'],
|
||||
closable: UseClosableParams['closable'],
|
||||
closeIcon: UseClosableParams['closeIcon'],
|
||||
defaultClosable: UseClosableParams['defaultClosable'],
|
||||
];
|
||||
|
||||
describe('hooks test', () => {
|
||||
@ -81,7 +81,7 @@ describe('hooks test', () => {
|
||||
res: [false, ''],
|
||||
},
|
||||
|
||||
// test case like: <Component closeIcon={null | false | element} />
|
||||
// test case like: <Component closeIcon={null | false | element | true} />
|
||||
{
|
||||
params: [undefined, null, undefined],
|
||||
res: [false, ''],
|
||||
@ -90,6 +90,10 @@ describe('hooks test', () => {
|
||||
params: [undefined, false, undefined],
|
||||
res: [false, ''],
|
||||
},
|
||||
{
|
||||
params: [undefined, true, undefined],
|
||||
res: [true, '.anticon-close'],
|
||||
},
|
||||
{
|
||||
params: [
|
||||
undefined,
|
||||
@ -138,11 +142,16 @@ describe('hooks test', () => {
|
||||
React.isValidElement(params[1]) ? 'element' : params[1]
|
||||
},defaultClosable=${params[2]}. the result should be ${res}`, () => {
|
||||
const App = () => {
|
||||
const [closable, closeIcon] = useClosable({
|
||||
closable: params[0],
|
||||
closeIcon: params[1],
|
||||
defaultClosable: params[2],
|
||||
});
|
||||
const [closable, closeIcon] = useClosable(
|
||||
{
|
||||
closable: params[0],
|
||||
closeIcon: params[1],
|
||||
},
|
||||
null,
|
||||
{
|
||||
closable: params[2],
|
||||
},
|
||||
);
|
||||
useEffect(() => {
|
||||
expect(closable).toBe(res[0]);
|
||||
}, [closable]);
|
||||
@ -159,10 +168,15 @@ describe('hooks test', () => {
|
||||
|
||||
it('useClosable with defaultCloseIcon', () => {
|
||||
const App = () => {
|
||||
const [closable, closeIcon] = useClosable({
|
||||
closable: true,
|
||||
defaultCloseIcon: <CloseOutlined className="custom-close-icon" />,
|
||||
});
|
||||
const [closable, closeIcon] = useClosable(
|
||||
{
|
||||
closable: true,
|
||||
},
|
||||
null,
|
||||
{
|
||||
closeIcon: <CloseOutlined className="custom-close-icon" />,
|
||||
},
|
||||
);
|
||||
useEffect(() => {
|
||||
expect(closable).toBe(true);
|
||||
}, [closable]);
|
||||
@ -171,16 +185,37 @@ describe('hooks test', () => {
|
||||
const { container } = render(<App />);
|
||||
expect(container.querySelector('.custom-close-icon')).toBeTruthy();
|
||||
});
|
||||
it('useClosable without defaultCloseIcon', () => {
|
||||
const App = () => {
|
||||
const [closable, closeIcon] = useClosable(
|
||||
{
|
||||
closable: true,
|
||||
},
|
||||
null,
|
||||
);
|
||||
useEffect(() => {
|
||||
expect(closable).toBe(true);
|
||||
}, [closable]);
|
||||
return <div>hooks test {closeIcon}</div>;
|
||||
};
|
||||
const { container } = render(<App />);
|
||||
expect(container.querySelector('.anticon-close')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('useClosable with customCloseIconRender', () => {
|
||||
const App = () => {
|
||||
const customCloseIconRender = (icon: React.ReactNode) => (
|
||||
<span className="custom-close-wrapper">{icon}</span>
|
||||
);
|
||||
const [closable, closeIcon] = useClosable({
|
||||
closable: true,
|
||||
customCloseIconRender,
|
||||
});
|
||||
const [closable, closeIcon] = useClosable(
|
||||
{
|
||||
closable: true,
|
||||
},
|
||||
null,
|
||||
{
|
||||
closeIconRender: customCloseIconRender,
|
||||
},
|
||||
);
|
||||
useEffect(() => {
|
||||
expect(closable).toBe(true);
|
||||
}, [closable]);
|
||||
|
@ -3,69 +3,174 @@ import React from 'react';
|
||||
import CloseOutlined from '@ant-design/icons/CloseOutlined';
|
||||
import pickAttrs from 'rc-util/lib/pickAttrs';
|
||||
|
||||
export type BaseClosableType = { closeIcon?: React.ReactNode } & React.AriaAttributes;
|
||||
export type ClosableType = boolean | BaseClosableType;
|
||||
|
||||
export type ContextClosable<T extends { closable?: ClosableType; closeIcon?: ReactNode } = any> =
|
||||
Partial<Pick<T, 'closable' | 'closeIcon'>>;
|
||||
|
||||
export function pickClosable<T extends { closable?: ClosableType; closeIcon?: ReactNode }>(
|
||||
context?: ContextClosable<T>,
|
||||
): ContextClosable<T> | undefined {
|
||||
if (!context) {
|
||||
return undefined;
|
||||
}
|
||||
return {
|
||||
closable: context.closable,
|
||||
closeIcon: context.closeIcon,
|
||||
};
|
||||
}
|
||||
|
||||
export type UseClosableParams = {
|
||||
closable?: boolean | ({ closeIcon?: React.ReactNode } & React.AriaAttributes);
|
||||
closable?: ClosableType;
|
||||
closeIcon?: ReactNode;
|
||||
defaultClosable?: boolean;
|
||||
defaultCloseIcon?: ReactNode;
|
||||
customCloseIconRender?: (closeIcon: ReactNode) => ReactNode;
|
||||
context?: ContextClosable;
|
||||
};
|
||||
|
||||
function useInnerClosable(
|
||||
closable?: UseClosableParams['closable'],
|
||||
closeIcon?: ReactNode,
|
||||
defaultClosable?: boolean,
|
||||
) {
|
||||
if (typeof closable === 'boolean') {
|
||||
return closable;
|
||||
}
|
||||
if (typeof closable === 'object') {
|
||||
return true;
|
||||
}
|
||||
if (closeIcon === undefined) {
|
||||
return !!defaultClosable;
|
||||
}
|
||||
return closeIcon !== false && closeIcon !== null;
|
||||
/** Convert `closable` and `closeIcon` to config object */
|
||||
function useClosableConfig(closableCollection?: ClosableCollection | null) {
|
||||
const { closable, closeIcon } = closableCollection || {};
|
||||
|
||||
return React.useMemo(() => {
|
||||
if (
|
||||
// If `closable`, whatever rest be should be true
|
||||
!closable &&
|
||||
(closable === false || closeIcon === false || closeIcon === null)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (closable === undefined && closeIcon === undefined) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let closableConfig: BaseClosableType = {
|
||||
closeIcon: typeof closeIcon !== 'boolean' && closeIcon !== null ? closeIcon : undefined,
|
||||
};
|
||||
if (closable && typeof closable === 'object') {
|
||||
closableConfig = {
|
||||
...closableConfig,
|
||||
...closable,
|
||||
};
|
||||
}
|
||||
|
||||
return closableConfig;
|
||||
}, [closable, closeIcon]);
|
||||
}
|
||||
|
||||
function useClosable({
|
||||
closable,
|
||||
closeIcon,
|
||||
customCloseIconRender,
|
||||
defaultCloseIcon = <CloseOutlined />,
|
||||
defaultClosable = false,
|
||||
}: UseClosableParams): [closable: boolean, closeIcon: React.ReactNode | null] {
|
||||
const mergedClosable = useInnerClosable(closable, closeIcon, defaultClosable);
|
||||
/**
|
||||
* Assign object without `undefined` field. Will skip if is `false`.
|
||||
* This helps to handle both closableConfig or false
|
||||
*/
|
||||
function assignWithoutUndefined<T extends object>(
|
||||
...objList: (Partial<T> | false | null | undefined)[]
|
||||
): Partial<T> {
|
||||
const target: Partial<T> = {};
|
||||
|
||||
if (!mergedClosable) {
|
||||
return [false, null];
|
||||
}
|
||||
const { closeIcon: closableIcon, ...restProps } =
|
||||
typeof closable === 'object'
|
||||
? closable
|
||||
: ({} as { closeIcon: React.ReactNode } & React.AriaAttributes);
|
||||
// Priority: closable.closeIcon > closeIcon > defaultCloseIcon
|
||||
const mergedCloseIcon: ReactNode = (() => {
|
||||
if (typeof closable === 'object' && closableIcon !== undefined) {
|
||||
return closableIcon;
|
||||
objList.forEach((obj) => {
|
||||
if (obj) {
|
||||
(Object.keys(obj) as (keyof T)[]).forEach((key) => {
|
||||
if (obj[key] !== undefined) {
|
||||
target[key] = obj[key];
|
||||
}
|
||||
});
|
||||
}
|
||||
return typeof closeIcon === 'boolean' || closeIcon === undefined || closeIcon === null
|
||||
? defaultCloseIcon
|
||||
: closeIcon;
|
||||
})();
|
||||
const ariaProps = pickAttrs(restProps, true);
|
||||
});
|
||||
|
||||
const plainCloseIcon = customCloseIconRender
|
||||
? customCloseIconRender(mergedCloseIcon)
|
||||
: mergedCloseIcon;
|
||||
return target;
|
||||
}
|
||||
|
||||
const closeIconWithAria = React.isValidElement(plainCloseIcon) ? (
|
||||
React.cloneElement(plainCloseIcon, ariaProps)
|
||||
) : (
|
||||
<span {...ariaProps}>{plainCloseIcon}</span>
|
||||
/** Collection contains the all the props related with closable. e.g. `closable`, `closeIcon` */
|
||||
interface ClosableCollection {
|
||||
closable?: ClosableType;
|
||||
closeIcon?: ReactNode;
|
||||
}
|
||||
|
||||
/** Use same object to support `useMemo` optimization */
|
||||
const EmptyFallbackCloseCollection: ClosableCollection = {};
|
||||
|
||||
export default function useClosable(
|
||||
propCloseCollection?: ClosableCollection,
|
||||
contextCloseCollection?: ClosableCollection | null,
|
||||
fallbackCloseCollection: ClosableCollection & {
|
||||
/**
|
||||
* Some components need to wrap CloseIcon twice,
|
||||
* this method will be executed once after the final CloseIcon is calculated
|
||||
*/
|
||||
closeIconRender?: (closeIcon: ReactNode) => ReactNode;
|
||||
} = EmptyFallbackCloseCollection,
|
||||
): [closable: boolean, closeIcon: React.ReactNode | null] {
|
||||
// Align the `props`, `context` `fallback` to config object first
|
||||
const propCloseConfig = useClosableConfig(propCloseCollection);
|
||||
const contextCloseConfig = useClosableConfig(contextCloseCollection);
|
||||
const mergedFallbackCloseCollection = React.useMemo(
|
||||
() => ({
|
||||
closeIcon: <CloseOutlined />,
|
||||
...fallbackCloseCollection,
|
||||
}),
|
||||
[fallbackCloseCollection],
|
||||
);
|
||||
|
||||
return [true, closeIconWithAria];
|
||||
}
|
||||
// Use fallback logic to fill the config
|
||||
const mergedClosableConfig = React.useMemo(() => {
|
||||
// ================ Props First ================
|
||||
// Skip if prop is disabled
|
||||
if (propCloseConfig === false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
export default useClosable;
|
||||
if (propCloseConfig) {
|
||||
return assignWithoutUndefined(
|
||||
mergedFallbackCloseCollection,
|
||||
contextCloseConfig,
|
||||
propCloseConfig,
|
||||
);
|
||||
}
|
||||
|
||||
// =============== Context Second ==============
|
||||
// Skip if context is disabled
|
||||
if (contextCloseConfig === false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (contextCloseConfig) {
|
||||
return assignWithoutUndefined(mergedFallbackCloseCollection, contextCloseConfig);
|
||||
}
|
||||
|
||||
// ============= Fallback Default ==============
|
||||
return !mergedFallbackCloseCollection.closable ? false : mergedFallbackCloseCollection;
|
||||
}, [propCloseConfig, contextCloseConfig, mergedFallbackCloseCollection]);
|
||||
|
||||
// Calculate the final closeIcon
|
||||
return React.useMemo(() => {
|
||||
if (mergedClosableConfig === false) {
|
||||
return [false, null];
|
||||
}
|
||||
|
||||
const { closeIconRender } = mergedFallbackCloseCollection;
|
||||
const { closeIcon } = mergedClosableConfig;
|
||||
|
||||
let mergedCloseIcon: ReactNode = closeIcon;
|
||||
if (mergedCloseIcon !== null && mergedCloseIcon !== undefined) {
|
||||
// Wrap the closeIcon if needed
|
||||
if (closeIconRender) {
|
||||
mergedCloseIcon = closeIconRender(closeIcon);
|
||||
}
|
||||
|
||||
// Wrap the closeIcon with aria props
|
||||
const ariaProps = pickAttrs(mergedClosableConfig, true);
|
||||
if (Object.keys(ariaProps).length) {
|
||||
mergedCloseIcon = React.isValidElement(mergedCloseIcon) ? (
|
||||
React.cloneElement(mergedCloseIcon, ariaProps)
|
||||
) : (
|
||||
<span {...ariaProps}>{mergedCloseIcon}</span>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return [true, mergedCloseIcon];
|
||||
}, [mergedClosableConfig, mergedFallbackCloseCollection]);
|
||||
}
|
||||
|
@ -1,5 +1,7 @@
|
||||
export const groupKeysMap = (keys: string[]) => {
|
||||
const map = new Map<string, number>();
|
||||
import type { TransferKey } from '../transfer/interface';
|
||||
|
||||
export const groupKeysMap = (keys: TransferKey[]) => {
|
||||
const map = new Map<TransferKey, number>();
|
||||
keys.forEach((key, index) => {
|
||||
map.set(key, index);
|
||||
});
|
||||
@ -7,7 +9,7 @@ export const groupKeysMap = (keys: string[]) => {
|
||||
};
|
||||
|
||||
export const groupDisabledKeysMap = <RecordType extends any[]>(dataSource: RecordType) => {
|
||||
const map = new Map<string, number>();
|
||||
const map = new Map<TransferKey, number>();
|
||||
dataSource.forEach(({ disabled, key }, index) => {
|
||||
if (disabled) {
|
||||
map.set(key, index);
|
||||
|
@ -13,12 +13,13 @@ import { replaceElement } from '../_util/reactNode';
|
||||
import { devUseWarning } from '../_util/warning';
|
||||
import { ConfigContext } from '../config-provider';
|
||||
import useStyle from './style';
|
||||
import type { ClosableType } from '../_util/hooks/useClosable';
|
||||
|
||||
export interface AlertProps {
|
||||
/** Type of Alert styles, options:`success`, `info`, `warning`, `error` */
|
||||
type?: 'success' | 'info' | 'warning' | 'error';
|
||||
/** Whether Alert can be closed */
|
||||
closable?: boolean | ({ closeIcon?: React.ReactNode } & React.AriaAttributes);
|
||||
closable?: ClosableType;
|
||||
/**
|
||||
* @deprecated please use `closable.closeIcon` instead.
|
||||
* Close text to show
|
||||
|
@ -218,8 +218,8 @@ const genSharedBadgeStyle: GenerateStyle<BadgeToken> = (token) => {
|
||||
},
|
||||
[`${componentCls}-status-processing`]: {
|
||||
overflow: 'visible',
|
||||
color: token.colorPrimary,
|
||||
backgroundColor: token.colorPrimary,
|
||||
color: token.colorInfo,
|
||||
backgroundColor: token.colorInfo,
|
||||
|
||||
'&::after': {
|
||||
position: 'absolute',
|
||||
|
3
components/calendar/locale/uz_UZ.ts
Normal file
3
components/calendar/locale/uz_UZ.ts
Normal file
@ -0,0 +1,3 @@
|
||||
import uzUZ from '../../date-picker/locale/uz_UZ';
|
||||
|
||||
export default uzUZ;
|
@ -727,7 +727,7 @@ exports[`renders components/carousel/demo/fade.tsx extend context correctly 1`]
|
||||
aria-hidden="false"
|
||||
class="slick-slide slick-active slick-current"
|
||||
data-index="0"
|
||||
style="outline: none; width: 0px; position: relative; left: 0px; opacity: 1; transition: opacity 500ms ease, visibility 500ms ease;"
|
||||
style="outline: none; width: 0px; position: relative; left: 0px; opacity: 1; z-index: 999; transition: opacity 500ms ease, visibility 500ms ease;"
|
||||
tabindex="-1"
|
||||
>
|
||||
<div>
|
||||
@ -747,7 +747,7 @@ exports[`renders components/carousel/demo/fade.tsx extend context correctly 1`]
|
||||
aria-hidden="true"
|
||||
class="slick-slide"
|
||||
data-index="1"
|
||||
style="outline: none; width: 0px; position: relative; left: 0px; opacity: 0; transition: opacity 500ms ease, visibility 500ms ease;"
|
||||
style="outline: none; width: 0px; position: relative; left: 0px; opacity: 0; z-index: 998; transition: opacity 500ms ease, visibility 500ms ease;"
|
||||
tabindex="-1"
|
||||
>
|
||||
<div>
|
||||
@ -767,7 +767,7 @@ exports[`renders components/carousel/demo/fade.tsx extend context correctly 1`]
|
||||
aria-hidden="true"
|
||||
class="slick-slide"
|
||||
data-index="2"
|
||||
style="outline: none; width: 0px; position: relative; left: 0px; opacity: 0; transition: opacity 500ms ease, visibility 500ms ease;"
|
||||
style="outline: none; width: 0px; position: relative; left: 0px; opacity: 0; z-index: 998; transition: opacity 500ms ease, visibility 500ms ease;"
|
||||
tabindex="-1"
|
||||
>
|
||||
<div>
|
||||
@ -787,7 +787,7 @@ exports[`renders components/carousel/demo/fade.tsx extend context correctly 1`]
|
||||
aria-hidden="true"
|
||||
class="slick-slide"
|
||||
data-index="3"
|
||||
style="outline: none; width: 0px; position: relative; left: 0px; opacity: 0; transition: opacity 500ms ease, visibility 500ms ease;"
|
||||
style="outline: none; width: 0px; position: relative; left: 0px; opacity: 0; z-index: 998; transition: opacity 500ms ease, visibility 500ms ease;"
|
||||
tabindex="-1"
|
||||
>
|
||||
<div>
|
||||
|
@ -721,7 +721,7 @@ exports[`renders components/carousel/demo/fade.tsx correctly 1`] = `
|
||||
aria-hidden="false"
|
||||
class="slick-slide slick-active slick-current"
|
||||
data-index="0"
|
||||
style="outline:none;width:11.11111111111111%;position:relative;left:0;opacity:1;transition:opacity 500ms ease, visibility 500ms ease"
|
||||
style="outline:none;width:11.11111111111111%;position:relative;left:0;opacity:1;z-index:999;transition:opacity 500ms ease, visibility 500ms ease"
|
||||
tabindex="-1"
|
||||
>
|
||||
<div>
|
||||
@ -741,7 +741,7 @@ exports[`renders components/carousel/demo/fade.tsx correctly 1`] = `
|
||||
aria-hidden="true"
|
||||
class="slick-slide"
|
||||
data-index="1"
|
||||
style="outline:none;width:11.11111111111111%;position:relative;left:-11px;opacity:0;transition:opacity 500ms ease, visibility 500ms ease"
|
||||
style="outline:none;width:11.11111111111111%;position:relative;left:-11px;opacity:0;z-index:998;transition:opacity 500ms ease, visibility 500ms ease"
|
||||
tabindex="-1"
|
||||
>
|
||||
<div>
|
||||
@ -761,7 +761,7 @@ exports[`renders components/carousel/demo/fade.tsx correctly 1`] = `
|
||||
aria-hidden="true"
|
||||
class="slick-slide"
|
||||
data-index="2"
|
||||
style="outline:none;width:11.11111111111111%;position:relative;left:-22px;opacity:0;transition:opacity 500ms ease, visibility 500ms ease"
|
||||
style="outline:none;width:11.11111111111111%;position:relative;left:-22px;opacity:0;z-index:998;transition:opacity 500ms ease, visibility 500ms ease"
|
||||
tabindex="-1"
|
||||
>
|
||||
<div>
|
||||
@ -781,7 +781,7 @@ exports[`renders components/carousel/demo/fade.tsx correctly 1`] = `
|
||||
aria-hidden="true"
|
||||
class="slick-slide"
|
||||
data-index="3"
|
||||
style="outline:none;width:11.11111111111111%;position:relative;left:-33px;opacity:0;transition:opacity 500ms ease, visibility 500ms ease"
|
||||
style="outline:none;width:11.11111111111111%;position:relative;left:-33px;opacity:0;z-index:998;transition:opacity 500ms ease, visibility 500ms ease"
|
||||
tabindex="-1"
|
||||
>
|
||||
<div>
|
||||
|
@ -6,6 +6,7 @@ exports[`Carousel rtl render component should be rendered correctly in RTL direc
|
||||
>
|
||||
<div
|
||||
class="slick-slider slick-initialized"
|
||||
dir="ltr"
|
||||
>
|
||||
<div
|
||||
class="slick-list"
|
||||
@ -25,6 +26,7 @@ exports[`Carousel should works for dotPosition bottom 1`] = `
|
||||
>
|
||||
<div
|
||||
class="slick-slider slick-initialized"
|
||||
dir="ltr"
|
||||
>
|
||||
<div
|
||||
class="slick-list"
|
||||
@ -33,6 +35,20 @@ exports[`Carousel should works for dotPosition bottom 1`] = `
|
||||
class="slick-track"
|
||||
style="opacity: 1; transform: translate3d(0px, 0px, 0px);"
|
||||
>
|
||||
<div
|
||||
aria-hidden="true"
|
||||
class="slick-slide slick-cloned"
|
||||
data-index="-1"
|
||||
style="width: 0px;"
|
||||
tabindex="-1"
|
||||
>
|
||||
<div>
|
||||
<div
|
||||
style="width: 100%; display: inline-block;"
|
||||
tabindex="-1"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
aria-hidden="false"
|
||||
class="slick-slide slick-active slick-current"
|
||||
@ -47,8 +63,34 @@ exports[`Carousel should works for dotPosition bottom 1`] = `
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
aria-hidden="true"
|
||||
class="slick-slide slick-cloned"
|
||||
data-index="1"
|
||||
style="width: 0px;"
|
||||
tabindex="-1"
|
||||
>
|
||||
<div>
|
||||
<div
|
||||
style="width: 100%; display: inline-block;"
|
||||
tabindex="-1"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<ul
|
||||
class="slick-dots slick-dots-bottom"
|
||||
style="display: block;"
|
||||
>
|
||||
<li
|
||||
class="slick-active"
|
||||
>
|
||||
<button>
|
||||
1
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
@ -59,14 +101,30 @@ exports[`Carousel should works for dotPosition left 1`] = `
|
||||
>
|
||||
<div
|
||||
class="slick-slider slick-vertical slick-initialized"
|
||||
dir="ltr"
|
||||
>
|
||||
<div
|
||||
class="slick-list"
|
||||
style="height: 0px;"
|
||||
>
|
||||
<div
|
||||
class="slick-track"
|
||||
style="opacity: 1; transform: translate3d(0px, 0px, 0px);"
|
||||
>
|
||||
<div
|
||||
aria-hidden="true"
|
||||
class="slick-slide slick-cloned"
|
||||
data-index="-1"
|
||||
style="width: 0px;"
|
||||
tabindex="-1"
|
||||
>
|
||||
<div>
|
||||
<div
|
||||
style="width: 100%; display: inline-block;"
|
||||
tabindex="-1"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
aria-hidden="false"
|
||||
class="slick-slide slick-active slick-current"
|
||||
@ -81,8 +139,34 @@ exports[`Carousel should works for dotPosition left 1`] = `
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
aria-hidden="true"
|
||||
class="slick-slide slick-cloned"
|
||||
data-index="1"
|
||||
style="width: 0px;"
|
||||
tabindex="-1"
|
||||
>
|
||||
<div>
|
||||
<div
|
||||
style="width: 100%; display: inline-block;"
|
||||
tabindex="-1"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<ul
|
||||
class="slick-dots slick-dots-left"
|
||||
style="display: block;"
|
||||
>
|
||||
<li
|
||||
class="slick-active"
|
||||
>
|
||||
<button>
|
||||
1
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
@ -93,14 +177,30 @@ exports[`Carousel should works for dotPosition right 1`] = `
|
||||
>
|
||||
<div
|
||||
class="slick-slider slick-vertical slick-initialized"
|
||||
dir="ltr"
|
||||
>
|
||||
<div
|
||||
class="slick-list"
|
||||
style="height: 0px;"
|
||||
>
|
||||
<div
|
||||
class="slick-track"
|
||||
style="opacity: 1; transform: translate3d(0px, 0px, 0px);"
|
||||
>
|
||||
<div
|
||||
aria-hidden="true"
|
||||
class="slick-slide slick-cloned"
|
||||
data-index="-1"
|
||||
style="width: 0px;"
|
||||
tabindex="-1"
|
||||
>
|
||||
<div>
|
||||
<div
|
||||
style="width: 100%; display: inline-block;"
|
||||
tabindex="-1"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
aria-hidden="false"
|
||||
class="slick-slide slick-active slick-current"
|
||||
@ -115,8 +215,34 @@ exports[`Carousel should works for dotPosition right 1`] = `
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
aria-hidden="true"
|
||||
class="slick-slide slick-cloned"
|
||||
data-index="1"
|
||||
style="width: 0px;"
|
||||
tabindex="-1"
|
||||
>
|
||||
<div>
|
||||
<div
|
||||
style="width: 100%; display: inline-block;"
|
||||
tabindex="-1"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<ul
|
||||
class="slick-dots slick-dots-right"
|
||||
style="display: block;"
|
||||
>
|
||||
<li
|
||||
class="slick-active"
|
||||
>
|
||||
<button>
|
||||
1
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
@ -127,6 +253,7 @@ exports[`Carousel should works for dotPosition top 1`] = `
|
||||
>
|
||||
<div
|
||||
class="slick-slider slick-initialized"
|
||||
dir="ltr"
|
||||
>
|
||||
<div
|
||||
class="slick-list"
|
||||
@ -135,6 +262,20 @@ exports[`Carousel should works for dotPosition top 1`] = `
|
||||
class="slick-track"
|
||||
style="opacity: 1; transform: translate3d(0px, 0px, 0px);"
|
||||
>
|
||||
<div
|
||||
aria-hidden="true"
|
||||
class="slick-slide slick-cloned"
|
||||
data-index="-1"
|
||||
style="width: 0px;"
|
||||
tabindex="-1"
|
||||
>
|
||||
<div>
|
||||
<div
|
||||
style="width: 100%; display: inline-block;"
|
||||
tabindex="-1"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
aria-hidden="false"
|
||||
class="slick-slide slick-active slick-current"
|
||||
@ -149,8 +290,34 @@ exports[`Carousel should works for dotPosition top 1`] = `
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
aria-hidden="true"
|
||||
class="slick-slide slick-cloned"
|
||||
data-index="1"
|
||||
style="width: 0px;"
|
||||
tabindex="-1"
|
||||
>
|
||||
<div>
|
||||
<div
|
||||
style="width: 100%; display: inline-block;"
|
||||
tabindex="-1"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<ul
|
||||
class="slick-dots slick-dots-top"
|
||||
style="display: block;"
|
||||
>
|
||||
<li
|
||||
class="slick-active"
|
||||
>
|
||||
<button>
|
||||
1
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
@ -88,6 +88,7 @@ Common props ref:[Common props](/docs/react/common-props)
|
||||
| onSearch | The callback function triggered when input changed | (search: string) => void | - | 4.17.0 |
|
||||
| dropdownMenuColumnStyle | The style of the drop-down menu column | CSSProperties | - | |
|
||||
| loadingIcon | The appearance of lazy loading (now is useless) | ReactNode | - | |
|
||||
| optionRender | Customize the rendering dropdown options | (option: Option) => React.ReactNode | - | 5.16.0 |
|
||||
|
||||
### showSearch
|
||||
|
||||
|
@ -88,6 +88,7 @@ demo:
|
||||
| searchValue | 设置搜索的值,需要与 `showSearch` 配合使用 | string | - | 4.17.0 |
|
||||
| onSearch | 监听搜索,返回输入的值 | (search: string) => void | - | 4.17.0 |
|
||||
| dropdownMenuColumnStyle | 下拉菜单列的样式 | CSSProperties | - | |
|
||||
| optionRender | 自定义渲染下拉选项 | (option: Option) => React.ReactNode | - | 5.16.0 |
|
||||
|
||||
### showSearch
|
||||
|
||||
|
@ -21,6 +21,7 @@ import Drawer from '../../drawer';
|
||||
import Dropdown from '../../dropdown';
|
||||
import Empty from '../../empty';
|
||||
import Flex from '../../flex';
|
||||
import FloatButton from '../../float-button';
|
||||
import Form from '../../form';
|
||||
import Image from '../../image';
|
||||
import Input from '../../input';
|
||||
@ -1085,6 +1086,59 @@ describe('ConfigProvider support style and className props', () => {
|
||||
expect(element?.querySelector<HTMLSpanElement>('.cp-test-closeIcon')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('Should Tag support aria-* in closable', () => {
|
||||
const { container } = render(
|
||||
<ConfigProvider
|
||||
tag={{
|
||||
closable: {
|
||||
closeIcon: <span className="cp-test-closeIcon">cp-test-closeIcon</span>,
|
||||
'aria-label': 'Close Tag',
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Tag>Test</Tag>
|
||||
<Tag.CheckableTag checked>CheckableTag</Tag.CheckableTag>
|
||||
</ConfigProvider>,
|
||||
);
|
||||
const element = container.querySelector<HTMLSpanElement>('.ant-tag');
|
||||
expect(element?.querySelector('.ant-tag-close-icon')).toBeTruthy();
|
||||
expect(element?.querySelector('.ant-tag-close-icon')?.getAttribute('aria-label')).toBe(
|
||||
'Close Tag',
|
||||
);
|
||||
expect(element?.querySelector('.cp-test-closeIcon')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('Should Tag hide closeIcon when closeIcon=false', () => {
|
||||
const { container } = render(
|
||||
<ConfigProvider
|
||||
tag={{
|
||||
closeIcon: false,
|
||||
}}
|
||||
>
|
||||
<Tag>Test</Tag>
|
||||
<Tag.CheckableTag checked>CheckableTag</Tag.CheckableTag>
|
||||
</ConfigProvider>,
|
||||
);
|
||||
const element = container.querySelector<HTMLSpanElement>('.ant-tag');
|
||||
expect(element?.querySelector('.ant-tag-close-icon')).toBeFalsy();
|
||||
});
|
||||
|
||||
it('Should Tag show default closeIcon when closeIcon=true', () => {
|
||||
const { container } = render(
|
||||
<ConfigProvider
|
||||
tag={{
|
||||
closeIcon: true,
|
||||
}}
|
||||
>
|
||||
<Tag>Test</Tag>
|
||||
<Tag.CheckableTag checked>CheckableTag</Tag.CheckableTag>
|
||||
</ConfigProvider>,
|
||||
);
|
||||
const element = container.querySelector<HTMLSpanElement>('.ant-tag');
|
||||
expect(element?.querySelector('.ant-tag-close-icon')).toBeTruthy();
|
||||
expect(element?.querySelector('.anticon-close')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('Should Table className & style works', () => {
|
||||
const { container } = render(
|
||||
<ConfigProvider
|
||||
@ -1507,4 +1561,18 @@ describe('ConfigProvider support style and className props', () => {
|
||||
const element = container.querySelector<HTMLSpanElement>(selectors);
|
||||
expect(element).toBeTruthy();
|
||||
});
|
||||
|
||||
it('Should FloatButton.Group closeIcon works', () => {
|
||||
const { container } = render(
|
||||
<ConfigProvider
|
||||
floatButtonGroup={{ closeIcon: <span className="test-cp-icon">test-cp-icon</span> }}
|
||||
>
|
||||
<FloatButton.Group trigger="click" open>
|
||||
<FloatButton />
|
||||
</FloatButton.Group>
|
||||
</ConfigProvider>,
|
||||
);
|
||||
const element = container.querySelector<HTMLSpanElement>('.test-cp-icon');
|
||||
expect(element).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
@ -9,6 +9,7 @@ import type { CardProps } from '../card';
|
||||
import type { CollapseProps } from '../collapse';
|
||||
import type { DrawerProps } from '../drawer';
|
||||
import type { FlexProps } from '../flex/interface';
|
||||
import type { FloatButtonGroupProps } from '../float-button/interface';
|
||||
import type { FormProps } from '../form/Form';
|
||||
import type { InputProps, TextAreaProps } from '../input';
|
||||
import type { Locale } from '../locale';
|
||||
@ -124,7 +125,7 @@ export type MenuConfig = ComponentStyleConfig & Pick<MenuProps, 'expandIcon'>;
|
||||
export type TourConfig = Pick<TourProps, 'closeIcon'>;
|
||||
|
||||
export type ModalConfig = ComponentStyleConfig &
|
||||
Pick<ModalProps, 'classNames' | 'styles' | 'closeIcon'>;
|
||||
Pick<ModalProps, 'classNames' | 'styles' | 'closeIcon' | 'closable'>;
|
||||
|
||||
export type TabsConfig = ComponentStyleConfig &
|
||||
Pick<TabsProps, 'indicator' | 'indicatorSize' | 'moreIcon' | 'addIcon' | 'removeIcon'>;
|
||||
@ -143,7 +144,7 @@ export type ButtonConfig = ComponentStyleConfig & Pick<ButtonProps, 'classNames'
|
||||
|
||||
export type NotificationConfig = ComponentStyleConfig & Pick<ArgsProps, 'closeIcon'>;
|
||||
|
||||
export type TagConfig = ComponentStyleConfig & Pick<TagProps, 'closeIcon'>;
|
||||
export type TagConfig = ComponentStyleConfig & Pick<TagProps, 'closeIcon' | 'closable'>;
|
||||
|
||||
export type CardConfig = ComponentStyleConfig & Pick<CardProps, 'classNames' | 'styles'>;
|
||||
|
||||
@ -157,6 +158,8 @@ export type TransferConfig = ComponentStyleConfig & Pick<TransferProps, 'selecti
|
||||
export type FormConfig = ComponentStyleConfig &
|
||||
Pick<FormProps, 'requiredMark' | 'colon' | 'scrollToFirstError' | 'validateMessages'>;
|
||||
|
||||
export type FloatButtonGroupConfig = Pick<FloatButtonGroupProps, 'closeIcon'>;
|
||||
|
||||
export type PaginationConfig = ComponentStyleConfig & Pick<PaginationProps, 'showSizeChanger'>;
|
||||
|
||||
export type SelectConfig = ComponentStyleConfig & Pick<SelectProps, 'showSearch'>;
|
||||
@ -213,6 +216,7 @@ export interface ConfigConsumerProps {
|
||||
carousel?: ComponentStyleConfig;
|
||||
cascader?: ComponentStyleConfig;
|
||||
collapse?: CollapseConfig;
|
||||
floatButtonGroup?: FloatButtonGroupConfig;
|
||||
typography?: ComponentStyleConfig;
|
||||
skeleton?: ComponentStyleConfig;
|
||||
spin?: ComponentStyleConfig;
|
||||
|
@ -121,6 +121,7 @@ const {
|
||||
| dropdown | Set Dropdown common props | { className?: string, style?: React.CSSProperties } | - | 5.11.0 |
|
||||
| empty | Set Empty common props | { className?: string, style?: React.CSSProperties } | - | 5.7.0 |
|
||||
| flex | Set Flex common props | { className?: string, style?: React.CSSProperties, vertical?: boolean } | - | 5.10.0 |
|
||||
| floatButtonGroup | Set FloatButton.Group common props | { closeIcon?: React.ReactNode } | - | 5.16.0 |
|
||||
| form | Set Form common props | { className?: string, style?: React.CSSProperties, validateMessages?: [ValidateMessages](/components/form/#validatemessages), requiredMark?: boolean \| `optional`, scrollToFirstError?: boolean \| [Options](https://github.com/stipsan/scroll-into-view-if-needed/tree/ece40bd9143f48caf4b99503425ecb16b0ad8249#options) } | - | requiredMark: 4.8.0; colon: 4.18.0; scrollToFirstError: 5.2.0; className: 5.7.0; style: 5.7.0 |
|
||||
| image | Set Image common props | { className?: string, style?: React.CSSProperties, preview?: { closeIcon?: React.ReactNode } } | - | 5.7.0, closeIcon: 5.14.0 |
|
||||
| input | Set Input common props | { autoComplete?: string, className?: string, style?: React.CSSProperties, allowClear?: boolean \| { clearIcon?: ReactNode } } | - | 4.2.0, allowClear: 5.15.0 |
|
||||
|
@ -26,6 +26,7 @@ import type {
|
||||
DirectionType,
|
||||
DrawerConfig,
|
||||
FlexConfig,
|
||||
FloatButtonGroupConfig,
|
||||
FormConfig,
|
||||
ImageConfig,
|
||||
InputConfig,
|
||||
@ -178,6 +179,7 @@ export interface ConfigProviderProps {
|
||||
slider?: ComponentStyleConfig;
|
||||
breadcrumb?: ComponentStyleConfig;
|
||||
menu?: MenuConfig;
|
||||
floatButtonGroup?: FloatButtonGroupConfig;
|
||||
checkbox?: ComponentStyleConfig;
|
||||
descriptions?: ComponentStyleConfig;
|
||||
empty?: ComponentStyleConfig;
|
||||
@ -362,6 +364,7 @@ const ProviderChildren: React.FC<ProviderChildrenProps> = (props) => {
|
||||
dropdown,
|
||||
warning: warningConfig,
|
||||
tour,
|
||||
floatButtonGroup,
|
||||
} = props;
|
||||
|
||||
// =================================== Context ===================================
|
||||
@ -457,6 +460,7 @@ const ProviderChildren: React.FC<ProviderChildrenProps> = (props) => {
|
||||
dropdown,
|
||||
warning: warningConfig,
|
||||
tour,
|
||||
floatButtonGroup,
|
||||
};
|
||||
|
||||
const config: ConfigConsumerProps = {
|
||||
|
@ -123,6 +123,7 @@ const {
|
||||
| dropdown | 设置 Dropdown 组件的通用属性 | { className?: string, style?: React.CSSProperties } | - | 5.11.0 |
|
||||
| empty | 设置 Empty 组件的通用属性 | { className?: string, style?: React.CSSProperties } | - | 5.7.0 |
|
||||
| flex | 设置 Flex 组件的通用属性 | { className?: string, style?: React.CSSProperties, vertical?: boolean } | - | 5.10.0 |
|
||||
| floatButtonGroup | 设置 FloatButton.Group 组件的通用属性 | { closeIcon?: React.ReactNode } | - | 5.16.0 |
|
||||
| form | 设置 Form 组件的通用属性 | { className?: string, style?: React.CSSProperties, validateMessages?: [ValidateMessages](/components/form-cn#validatemessages), requiredMark?: boolean \| `optional`, colon?: boolean, scrollToFirstError?: boolean \| [Options](https://github.com/stipsan/scroll-into-view-if-needed/tree/ece40bd9143f48caf4b99503425ecb16b0ad8249#options)} | - | requiredMark: 4.8.0; colon: 4.18.0; scrollToFirstError: 5.2.0; className: 5.7.0; style: 5.7.0 |
|
||||
| image | 设置 Image 组件的通用属性 | { className?: string, style?: React.CSSProperties, preview?: { closeIcon?: React.ReactNode } } | - | 5.7.0, closeIcon: 5.14.0 |
|
||||
| input | 设置 Input 组件的通用属性 | { autoComplete?: string, className?: string, style?: React.CSSProperties, allowClear?: boolean \| { clearIcon?: ReactNode } } | - | 5.7.0, allowClear: 5.15.0 |
|
||||
|
28
components/date-picker/locale/uz_UZ.ts
Normal file
28
components/date-picker/locale/uz_UZ.ts
Normal file
@ -0,0 +1,28 @@
|
||||
import CalendarLocale from 'rc-picker/lib/locale/uz_UZ';
|
||||
|
||||
import TimePickerLocale from '../../time-picker/locale/uz_UZ';
|
||||
import type { PickerLocale } from '../generatePicker';
|
||||
|
||||
// Merge into a locale object
|
||||
const locale: PickerLocale = {
|
||||
lang: {
|
||||
placeholder: 'Sanani tanlang',
|
||||
yearPlaceholder: 'Yilni tanlang',
|
||||
quarterPlaceholder: 'Chorakni tanlang',
|
||||
monthPlaceholder: 'Oyni tanlang',
|
||||
weekPlaceholder: 'Haftani tanlang',
|
||||
rangePlaceholder: ['Boshlanish sanasi', 'Tugallanish sanasi'],
|
||||
rangeYearPlaceholder: ['Boshlanish yili', 'Tugallanish yili'],
|
||||
rangeMonthPlaceholder: ['Boshlanish oyi', 'Tugallanish oyi'],
|
||||
rangeWeekPlaceholder: ['Boshlanish haftasi', 'Tugallanish haftasi'],
|
||||
...CalendarLocale,
|
||||
},
|
||||
timePickerLocale: {
|
||||
...TimePickerLocale,
|
||||
},
|
||||
};
|
||||
|
||||
// All settings at:
|
||||
// https://github.com/ant-design/ant-design/blob/master/components/date-picker/locale/example.json
|
||||
|
||||
export default locale;
|
@ -1,7 +1,8 @@
|
||||
import * as React from 'react';
|
||||
import classNames from 'classnames';
|
||||
import type { DrawerProps as RCDrawerProps } from 'rc-drawer';
|
||||
import useClosable from '../_util/hooks/useClosable';
|
||||
|
||||
import useClosable, { pickClosable, type ClosableType } from '../_util/hooks/useClosable';
|
||||
import { ConfigContext } from '../config-provider';
|
||||
|
||||
export interface DrawerClassNames extends NonNullable<RCDrawerProps['classNames']> {
|
||||
@ -29,7 +30,7 @@ export interface DrawerPanelProps {
|
||||
*
|
||||
* `<Drawer closeIcon={false} />`
|
||||
*/
|
||||
closable?: boolean | ({ closeIcon?: React.ReactNode } & React.AriaAttributes);
|
||||
closable?: ClosableType;
|
||||
closeIcon?: React.ReactNode;
|
||||
onClose?: RCDrawerProps['onClose'];
|
||||
|
||||
@ -57,8 +58,6 @@ const DrawerPanel: React.FC<DrawerPanelProps> = (props) => {
|
||||
title,
|
||||
footer,
|
||||
extra,
|
||||
closeIcon,
|
||||
closable,
|
||||
onClose,
|
||||
headerStyle,
|
||||
bodyStyle,
|
||||
@ -78,19 +77,14 @@ const DrawerPanel: React.FC<DrawerPanelProps> = (props) => {
|
||||
[onClose],
|
||||
);
|
||||
|
||||
const mergedContextCloseIcon = React.useMemo(() => {
|
||||
if (typeof drawerContext?.closable === 'object' && drawerContext.closable.closeIcon) {
|
||||
return drawerContext.closable.closeIcon;
|
||||
}
|
||||
return drawerContext?.closeIcon;
|
||||
}, [drawerContext?.closable, drawerContext?.closeIcon]);
|
||||
|
||||
const [mergedClosable, mergedCloseIcon] = useClosable({
|
||||
closable: closable ?? drawerContext?.closable,
|
||||
closeIcon: typeof closeIcon !== 'undefined' ? closeIcon : mergedContextCloseIcon,
|
||||
customCloseIconRender,
|
||||
defaultClosable: true,
|
||||
});
|
||||
const [mergedClosable, mergedCloseIcon] = useClosable(
|
||||
pickClosable(props),
|
||||
pickClosable(drawerContext),
|
||||
{
|
||||
closable: true,
|
||||
closeIconRender: customCloseIconRender,
|
||||
},
|
||||
);
|
||||
|
||||
const headerNode = React.useMemo<React.ReactNode>(() => {
|
||||
if (!title && !mergedClosable) {
|
||||
|
@ -8,11 +8,11 @@ import useMergedState from 'rc-util/lib/hooks/useMergedState';
|
||||
import { devUseWarning } from '../_util/warning';
|
||||
import type { ConfigConsumerProps } from '../config-provider';
|
||||
import { ConfigContext } from '../config-provider';
|
||||
import useCSSVarCls from '../config-provider/hooks/useCSSVarCls';
|
||||
import { FloatButtonGroupProvider } from './context';
|
||||
import FloatButton, { floatButtonPrefixCls } from './FloatButton';
|
||||
import type { FloatButtonGroupProps, FloatButtonRef } from './interface';
|
||||
import useStyle from './style';
|
||||
import useCSSVarCls from '../config-provider/hooks/useCSSVarCls';
|
||||
|
||||
const FloatButtonGroup: React.FC<FloatButtonGroupProps> = (props) => {
|
||||
const {
|
||||
@ -22,7 +22,7 @@ const FloatButtonGroup: React.FC<FloatButtonGroupProps> = (props) => {
|
||||
shape = 'circle',
|
||||
type = 'default',
|
||||
icon = <FileTextOutlined />,
|
||||
closeIcon = <CloseOutlined />,
|
||||
closeIcon,
|
||||
description,
|
||||
trigger,
|
||||
children,
|
||||
@ -31,7 +31,11 @@ const FloatButtonGroup: React.FC<FloatButtonGroupProps> = (props) => {
|
||||
...floatButtonProps
|
||||
} = props;
|
||||
|
||||
const { direction, getPrefixCls } = useContext<ConfigConsumerProps>(ConfigContext);
|
||||
const { direction, getPrefixCls, floatButtonGroup } =
|
||||
useContext<ConfigConsumerProps>(ConfigContext);
|
||||
|
||||
const mergedCloseIcon = closeIcon ?? floatButtonGroup?.closeIcon ?? <CloseOutlined />;
|
||||
|
||||
const prefixCls = getPrefixCls(floatButtonPrefixCls, customizePrefixCls);
|
||||
const rootCls = useCSSVarCls(prefixCls);
|
||||
const [wrapCSSVar, hashId, cssVarCls] = useStyle(prefixCls, rootCls);
|
||||
@ -120,7 +124,7 @@ const FloatButtonGroup: React.FC<FloatButtonGroupProps> = (props) => {
|
||||
ref={floatButtonRef}
|
||||
type={type}
|
||||
shape={shape}
|
||||
icon={open ? closeIcon : icon}
|
||||
icon={open ? mergedCloseIcon : icon}
|
||||
description={description}
|
||||
aria-label={props['aria-label']}
|
||||
{...floatButtonProps}
|
||||
|
@ -28500,6 +28500,216 @@ exports[`renders components/form/demo/validate-static.tsx extend context correct
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="ant-form-item ant-form-item-has-feedback ant-form-item-has-success"
|
||||
>
|
||||
<div
|
||||
class="ant-row ant-form-item-row"
|
||||
>
|
||||
<div
|
||||
class="ant-col ant-form-item-label ant-col-xs-24 ant-col-sm-6"
|
||||
>
|
||||
<label
|
||||
class=""
|
||||
title="Success"
|
||||
>
|
||||
Success
|
||||
</label>
|
||||
</div>
|
||||
<div
|
||||
class="ant-col ant-form-item-control ant-col-xs-24 ant-col-sm-14"
|
||||
>
|
||||
<div
|
||||
class="ant-form-item-control-input"
|
||||
>
|
||||
<div
|
||||
class="ant-form-item-control-input-content"
|
||||
>
|
||||
<div
|
||||
class="ant-otp"
|
||||
>
|
||||
<input
|
||||
class="ant-input ant-input-outlined ant-input-status-success ant-otp-input"
|
||||
size="1"
|
||||
type="text"
|
||||
value=""
|
||||
/>
|
||||
<input
|
||||
class="ant-input ant-input-outlined ant-input-status-success ant-otp-input"
|
||||
size="1"
|
||||
type="text"
|
||||
value=""
|
||||
/>
|
||||
<input
|
||||
class="ant-input ant-input-outlined ant-input-status-success ant-otp-input"
|
||||
size="1"
|
||||
type="text"
|
||||
value=""
|
||||
/>
|
||||
<input
|
||||
class="ant-input ant-input-outlined ant-input-status-success ant-otp-input"
|
||||
size="1"
|
||||
type="text"
|
||||
value=""
|
||||
/>
|
||||
<input
|
||||
class="ant-input ant-input-outlined ant-input-status-success ant-otp-input"
|
||||
size="1"
|
||||
type="text"
|
||||
value=""
|
||||
/>
|
||||
<input
|
||||
class="ant-input ant-input-outlined ant-input-status-success ant-otp-input"
|
||||
size="1"
|
||||
type="text"
|
||||
value=""
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="ant-form-item ant-form-item-has-feedback ant-form-item-has-warning"
|
||||
>
|
||||
<div
|
||||
class="ant-row ant-form-item-row"
|
||||
>
|
||||
<div
|
||||
class="ant-col ant-form-item-label ant-col-xs-24 ant-col-sm-6"
|
||||
>
|
||||
<label
|
||||
class=""
|
||||
title="Warning"
|
||||
>
|
||||
Warning
|
||||
</label>
|
||||
</div>
|
||||
<div
|
||||
class="ant-col ant-form-item-control ant-col-xs-24 ant-col-sm-14"
|
||||
>
|
||||
<div
|
||||
class="ant-form-item-control-input"
|
||||
>
|
||||
<div
|
||||
class="ant-form-item-control-input-content"
|
||||
>
|
||||
<div
|
||||
class="ant-otp"
|
||||
>
|
||||
<input
|
||||
class="ant-input ant-input-outlined ant-input-status-warning ant-otp-input"
|
||||
size="1"
|
||||
type="text"
|
||||
value=""
|
||||
/>
|
||||
<input
|
||||
class="ant-input ant-input-outlined ant-input-status-warning ant-otp-input"
|
||||
size="1"
|
||||
type="text"
|
||||
value=""
|
||||
/>
|
||||
<input
|
||||
class="ant-input ant-input-outlined ant-input-status-warning ant-otp-input"
|
||||
size="1"
|
||||
type="text"
|
||||
value=""
|
||||
/>
|
||||
<input
|
||||
class="ant-input ant-input-outlined ant-input-status-warning ant-otp-input"
|
||||
size="1"
|
||||
type="text"
|
||||
value=""
|
||||
/>
|
||||
<input
|
||||
class="ant-input ant-input-outlined ant-input-status-warning ant-otp-input"
|
||||
size="1"
|
||||
type="text"
|
||||
value=""
|
||||
/>
|
||||
<input
|
||||
class="ant-input ant-input-outlined ant-input-status-warning ant-otp-input"
|
||||
size="1"
|
||||
type="text"
|
||||
value=""
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="ant-form-item ant-form-item-has-feedback ant-form-item-has-error"
|
||||
>
|
||||
<div
|
||||
class="ant-row ant-form-item-row"
|
||||
>
|
||||
<div
|
||||
class="ant-col ant-form-item-label ant-col-xs-24 ant-col-sm-6"
|
||||
>
|
||||
<label
|
||||
class=""
|
||||
title="Error"
|
||||
>
|
||||
Error
|
||||
</label>
|
||||
</div>
|
||||
<div
|
||||
class="ant-col ant-form-item-control ant-col-xs-24 ant-col-sm-14"
|
||||
>
|
||||
<div
|
||||
class="ant-form-item-control-input"
|
||||
>
|
||||
<div
|
||||
class="ant-form-item-control-input-content"
|
||||
>
|
||||
<div
|
||||
class="ant-otp"
|
||||
>
|
||||
<input
|
||||
class="ant-input ant-input-outlined ant-input-status-error ant-otp-input"
|
||||
size="1"
|
||||
type="text"
|
||||
value=""
|
||||
/>
|
||||
<input
|
||||
class="ant-input ant-input-outlined ant-input-status-error ant-otp-input"
|
||||
size="1"
|
||||
type="text"
|
||||
value=""
|
||||
/>
|
||||
<input
|
||||
class="ant-input ant-input-outlined ant-input-status-error ant-otp-input"
|
||||
size="1"
|
||||
type="text"
|
||||
value=""
|
||||
/>
|
||||
<input
|
||||
class="ant-input ant-input-outlined ant-input-status-error ant-otp-input"
|
||||
size="1"
|
||||
type="text"
|
||||
value=""
|
||||
/>
|
||||
<input
|
||||
class="ant-input ant-input-outlined ant-input-status-error ant-otp-input"
|
||||
size="1"
|
||||
type="text"
|
||||
value=""
|
||||
/>
|
||||
<input
|
||||
class="ant-input ant-input-outlined ant-input-status-error ant-otp-input"
|
||||
size="1"
|
||||
type="text"
|
||||
value=""
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="ant-form-item ant-form-item-has-feedback ant-form-item-has-error"
|
||||
>
|
||||
|
@ -11508,6 +11508,216 @@ exports[`renders components/form/demo/validate-static.tsx correctly 1`] = `
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="ant-form-item ant-form-item-has-feedback ant-form-item-has-success"
|
||||
>
|
||||
<div
|
||||
class="ant-row ant-form-item-row"
|
||||
>
|
||||
<div
|
||||
class="ant-col ant-form-item-label ant-col-xs-24 ant-col-sm-6"
|
||||
>
|
||||
<label
|
||||
class=""
|
||||
title="Success"
|
||||
>
|
||||
Success
|
||||
</label>
|
||||
</div>
|
||||
<div
|
||||
class="ant-col ant-form-item-control ant-col-xs-24 ant-col-sm-14"
|
||||
>
|
||||
<div
|
||||
class="ant-form-item-control-input"
|
||||
>
|
||||
<div
|
||||
class="ant-form-item-control-input-content"
|
||||
>
|
||||
<div
|
||||
class="ant-otp"
|
||||
>
|
||||
<input
|
||||
class="ant-input ant-input-outlined ant-input-status-success ant-otp-input"
|
||||
size="1"
|
||||
type="text"
|
||||
value=""
|
||||
/>
|
||||
<input
|
||||
class="ant-input ant-input-outlined ant-input-status-success ant-otp-input"
|
||||
size="1"
|
||||
type="text"
|
||||
value=""
|
||||
/>
|
||||
<input
|
||||
class="ant-input ant-input-outlined ant-input-status-success ant-otp-input"
|
||||
size="1"
|
||||
type="text"
|
||||
value=""
|
||||
/>
|
||||
<input
|
||||
class="ant-input ant-input-outlined ant-input-status-success ant-otp-input"
|
||||
size="1"
|
||||
type="text"
|
||||
value=""
|
||||
/>
|
||||
<input
|
||||
class="ant-input ant-input-outlined ant-input-status-success ant-otp-input"
|
||||
size="1"
|
||||
type="text"
|
||||
value=""
|
||||
/>
|
||||
<input
|
||||
class="ant-input ant-input-outlined ant-input-status-success ant-otp-input"
|
||||
size="1"
|
||||
type="text"
|
||||
value=""
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="ant-form-item ant-form-item-has-feedback ant-form-item-has-warning"
|
||||
>
|
||||
<div
|
||||
class="ant-row ant-form-item-row"
|
||||
>
|
||||
<div
|
||||
class="ant-col ant-form-item-label ant-col-xs-24 ant-col-sm-6"
|
||||
>
|
||||
<label
|
||||
class=""
|
||||
title="Warning"
|
||||
>
|
||||
Warning
|
||||
</label>
|
||||
</div>
|
||||
<div
|
||||
class="ant-col ant-form-item-control ant-col-xs-24 ant-col-sm-14"
|
||||
>
|
||||
<div
|
||||
class="ant-form-item-control-input"
|
||||
>
|
||||
<div
|
||||
class="ant-form-item-control-input-content"
|
||||
>
|
||||
<div
|
||||
class="ant-otp"
|
||||
>
|
||||
<input
|
||||
class="ant-input ant-input-outlined ant-input-status-warning ant-otp-input"
|
||||
size="1"
|
||||
type="text"
|
||||
value=""
|
||||
/>
|
||||
<input
|
||||
class="ant-input ant-input-outlined ant-input-status-warning ant-otp-input"
|
||||
size="1"
|
||||
type="text"
|
||||
value=""
|
||||
/>
|
||||
<input
|
||||
class="ant-input ant-input-outlined ant-input-status-warning ant-otp-input"
|
||||
size="1"
|
||||
type="text"
|
||||
value=""
|
||||
/>
|
||||
<input
|
||||
class="ant-input ant-input-outlined ant-input-status-warning ant-otp-input"
|
||||
size="1"
|
||||
type="text"
|
||||
value=""
|
||||
/>
|
||||
<input
|
||||
class="ant-input ant-input-outlined ant-input-status-warning ant-otp-input"
|
||||
size="1"
|
||||
type="text"
|
||||
value=""
|
||||
/>
|
||||
<input
|
||||
class="ant-input ant-input-outlined ant-input-status-warning ant-otp-input"
|
||||
size="1"
|
||||
type="text"
|
||||
value=""
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="ant-form-item ant-form-item-has-feedback ant-form-item-has-error"
|
||||
>
|
||||
<div
|
||||
class="ant-row ant-form-item-row"
|
||||
>
|
||||
<div
|
||||
class="ant-col ant-form-item-label ant-col-xs-24 ant-col-sm-6"
|
||||
>
|
||||
<label
|
||||
class=""
|
||||
title="Error"
|
||||
>
|
||||
Error
|
||||
</label>
|
||||
</div>
|
||||
<div
|
||||
class="ant-col ant-form-item-control ant-col-xs-24 ant-col-sm-14"
|
||||
>
|
||||
<div
|
||||
class="ant-form-item-control-input"
|
||||
>
|
||||
<div
|
||||
class="ant-form-item-control-input-content"
|
||||
>
|
||||
<div
|
||||
class="ant-otp"
|
||||
>
|
||||
<input
|
||||
class="ant-input ant-input-outlined ant-input-status-error ant-otp-input"
|
||||
size="1"
|
||||
type="text"
|
||||
value=""
|
||||
/>
|
||||
<input
|
||||
class="ant-input ant-input-outlined ant-input-status-error ant-otp-input"
|
||||
size="1"
|
||||
type="text"
|
||||
value=""
|
||||
/>
|
||||
<input
|
||||
class="ant-input ant-input-outlined ant-input-status-error ant-otp-input"
|
||||
size="1"
|
||||
type="text"
|
||||
value=""
|
||||
/>
|
||||
<input
|
||||
class="ant-input ant-input-outlined ant-input-status-error ant-otp-input"
|
||||
size="1"
|
||||
type="text"
|
||||
value=""
|
||||
/>
|
||||
<input
|
||||
class="ant-input ant-input-outlined ant-input-status-error ant-otp-input"
|
||||
size="1"
|
||||
type="text"
|
||||
value=""
|
||||
/>
|
||||
<input
|
||||
class="ant-input ant-input-outlined ant-input-status-error ant-otp-input"
|
||||
size="1"
|
||||
type="text"
|
||||
value=""
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="ant-form-item ant-form-item-has-feedback ant-form-item-has-error"
|
||||
>
|
||||
|
@ -136,6 +136,17 @@ const App: React.FC = () => (
|
||||
<Input.Password allowClear placeholder="with input password and allowClear" />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item label="Success" hasFeedback validateStatus="success">
|
||||
<Input.OTP />
|
||||
</Form.Item>
|
||||
<Form.Item label="Warning" hasFeedback validateStatus="warning">
|
||||
<Input.OTP />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item label="Error" hasFeedback validateStatus="error">
|
||||
<Input.OTP />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item label="Fail" validateStatus="error" hasFeedback>
|
||||
<Mentions />
|
||||
</Form.Item>
|
||||
|
@ -264,4 +264,8 @@ const Input = forwardRef<InputRef, InputProps>((props, ref) => {
|
||||
);
|
||||
});
|
||||
|
||||
if (process.env.NODE_ENV !== 'production') {
|
||||
Input.displayName = 'Input';
|
||||
}
|
||||
|
||||
export default Input;
|
||||
|
69
components/input/OTP/OTPInput.tsx
Normal file
69
components/input/OTP/OTPInput.tsx
Normal file
@ -0,0 +1,69 @@
|
||||
import * as React from 'react';
|
||||
import raf from 'rc-util/lib/raf';
|
||||
|
||||
import Input, { type InputProps, type InputRef } from '../Input';
|
||||
|
||||
export interface OTPInputProps extends Omit<InputProps, 'onChange'> {
|
||||
index: number;
|
||||
onChange: (index: number, value: string) => void;
|
||||
/** Tell parent to do active offset */
|
||||
onActiveChange: (nextIndex: number) => void;
|
||||
}
|
||||
|
||||
const OTPInput = React.forwardRef<InputRef, OTPInputProps>((props, ref) => {
|
||||
const { value, onChange, onActiveChange, index, ...restProps } = props;
|
||||
|
||||
const onInternalChange: React.ChangeEventHandler<HTMLInputElement> = (e) => {
|
||||
onChange(index, e.target.value);
|
||||
};
|
||||
|
||||
// ========================== Ref ===========================
|
||||
const inputRef = React.useRef<InputRef>(null);
|
||||
React.useImperativeHandle(ref, () => inputRef.current!);
|
||||
|
||||
// ========================= Focus ==========================
|
||||
const syncSelection = () => {
|
||||
raf(() => {
|
||||
const inputEle = inputRef.current?.input;
|
||||
if (document.activeElement === inputEle && inputEle) {
|
||||
inputEle.select();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// ======================== Keyboard ========================
|
||||
const onInternalKeyDown: React.KeyboardEventHandler<HTMLInputElement> = ({ key }) => {
|
||||
if (key === 'ArrowLeft') {
|
||||
onActiveChange(index - 1);
|
||||
} else if (key === 'ArrowRight') {
|
||||
onActiveChange(index + 1);
|
||||
}
|
||||
|
||||
syncSelection();
|
||||
};
|
||||
|
||||
const onInternalKeyUp: React.KeyboardEventHandler<HTMLInputElement> = (e) => {
|
||||
if (e.key === 'Backspace' && !value) {
|
||||
onActiveChange(index - 1);
|
||||
}
|
||||
|
||||
syncSelection();
|
||||
};
|
||||
|
||||
// ========================= Render =========================
|
||||
return (
|
||||
<Input
|
||||
{...restProps}
|
||||
ref={inputRef}
|
||||
value={value}
|
||||
onInput={onInternalChange}
|
||||
onFocus={syncSelection}
|
||||
onKeyDown={onInternalKeyDown}
|
||||
onKeyUp={onInternalKeyUp}
|
||||
onMouseDown={syncSelection}
|
||||
onMouseUp={syncSelection}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
export default OTPInput;
|
244
components/input/OTP/index.tsx
Normal file
244
components/input/OTP/index.tsx
Normal file
@ -0,0 +1,244 @@
|
||||
import * as React from 'react';
|
||||
import classNames from 'classnames';
|
||||
import { useEvent } from 'rc-util';
|
||||
import pickAttrs from 'rc-util/lib/pickAttrs';
|
||||
|
||||
import { getMergedStatus, type InputStatus } from '../../_util/statusUtils';
|
||||
import { ConfigContext } from '../../config-provider';
|
||||
import useCSSVarCls from '../../config-provider/hooks/useCSSVarCls';
|
||||
import useSize from '../../config-provider/hooks/useSize';
|
||||
import { type SizeType } from '../../config-provider/SizeContext';
|
||||
import { FormItemInputContext } from '../../form/context';
|
||||
import type { Variant } from '../../form/hooks/useVariants';
|
||||
import { type InputRef } from '../Input';
|
||||
import useStyle from '../style/otp';
|
||||
import OTPInput, { type OTPInputProps } from './OTPInput';
|
||||
|
||||
export interface OTPRef {
|
||||
focus: VoidFunction;
|
||||
blur: VoidFunction;
|
||||
nativeElement: HTMLDivElement;
|
||||
}
|
||||
|
||||
export interface OTPProps extends Omit<React.HTMLAttributes<HTMLDivElement>, 'onChange'> {
|
||||
prefixCls?: string;
|
||||
length?: number;
|
||||
|
||||
// Style
|
||||
variant?: Variant;
|
||||
rootClassName?: string;
|
||||
className?: string;
|
||||
style?: React.CSSProperties;
|
||||
size?: SizeType;
|
||||
|
||||
// Values
|
||||
defaultValue?: string;
|
||||
value?: string;
|
||||
onChange?: (value: string) => void;
|
||||
formatter?: (value: string) => string;
|
||||
|
||||
// Status
|
||||
disabled?: boolean;
|
||||
status?: InputStatus;
|
||||
}
|
||||
|
||||
function strToArr(str: string) {
|
||||
return str.split('');
|
||||
}
|
||||
|
||||
const OTP = React.forwardRef<OTPRef, OTPProps>((props, ref) => {
|
||||
const {
|
||||
prefixCls: customizePrefixCls,
|
||||
length = 6,
|
||||
size: customSize,
|
||||
defaultValue,
|
||||
value,
|
||||
onChange,
|
||||
formatter,
|
||||
variant,
|
||||
disabled,
|
||||
status: customStatus,
|
||||
autoFocus,
|
||||
...restProps
|
||||
} = props;
|
||||
|
||||
const { getPrefixCls, direction } = React.useContext(ConfigContext);
|
||||
const prefixCls = getPrefixCls('otp', customizePrefixCls);
|
||||
|
||||
const domAttrs = pickAttrs(restProps, {
|
||||
aria: true,
|
||||
data: true,
|
||||
attr: true,
|
||||
});
|
||||
|
||||
// ========================= Root =========================
|
||||
// Style
|
||||
const rootCls = useCSSVarCls(prefixCls);
|
||||
const [wrapCSSVar, hashId, cssVarCls] = useStyle(prefixCls, rootCls);
|
||||
|
||||
// ========================= Size =========================
|
||||
const mergedSize = useSize((ctx) => customSize ?? ctx);
|
||||
|
||||
// ======================== Status ========================
|
||||
const formContext = React.useContext(FormItemInputContext);
|
||||
const mergedStatus = getMergedStatus(formContext.status, customStatus);
|
||||
|
||||
const proxyFormContext = React.useMemo(
|
||||
() => ({
|
||||
...formContext,
|
||||
status: mergedStatus,
|
||||
hasFeedback: false,
|
||||
feedbackIcon: null,
|
||||
}),
|
||||
[formContext, mergedStatus],
|
||||
);
|
||||
|
||||
// ========================= Refs =========================
|
||||
const containerRef = React.useRef<HTMLDivElement>(null);
|
||||
|
||||
const refs = React.useRef<Record<number, InputRef | null>>({});
|
||||
|
||||
React.useImperativeHandle(ref, () => ({
|
||||
focus: () => {
|
||||
refs.current[0]?.focus();
|
||||
},
|
||||
blur: () => {
|
||||
for (let i = 0; i < length; i += 1) {
|
||||
refs.current[i]?.blur();
|
||||
}
|
||||
},
|
||||
nativeElement: containerRef.current!,
|
||||
}));
|
||||
|
||||
// ======================= Formatter ======================
|
||||
const internalFormatter = (txt: string) => (formatter ? formatter(txt) : txt);
|
||||
|
||||
// ======================== Values ========================
|
||||
const [valueCells, setValueCells] = React.useState<string[]>(
|
||||
strToArr(internalFormatter(defaultValue || '')),
|
||||
);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (value) {
|
||||
setValueCells(strToArr(value));
|
||||
}
|
||||
}, [value]);
|
||||
|
||||
const triggerValueCellsChange = useEvent((nextValueCells: string[]) => {
|
||||
setValueCells(nextValueCells);
|
||||
|
||||
// Trigger if all cells are filled
|
||||
if (
|
||||
onChange &&
|
||||
nextValueCells.length === length &&
|
||||
nextValueCells.every((c) => c) &&
|
||||
nextValueCells.some((c, index) => valueCells[index] !== c)
|
||||
) {
|
||||
onChange(nextValueCells.join(''));
|
||||
}
|
||||
});
|
||||
|
||||
const patchValue = useEvent((index: number, txt: string) => {
|
||||
let nextCells = [...valueCells];
|
||||
|
||||
// Fill cells till index
|
||||
for (let i = 0; i < index; i += 1) {
|
||||
if (!nextCells[i]) {
|
||||
nextCells[i] = '';
|
||||
}
|
||||
}
|
||||
|
||||
if (txt.length <= 1) {
|
||||
nextCells[index] = txt;
|
||||
} else {
|
||||
nextCells = nextCells.slice(0, index).concat(strToArr(txt));
|
||||
}
|
||||
nextCells = nextCells.slice(0, length);
|
||||
|
||||
// Clean the last empty cell
|
||||
for (let i = nextCells.length - 1; i >= 0; i -= 1) {
|
||||
if (nextCells[i]) {
|
||||
break;
|
||||
}
|
||||
nextCells.pop();
|
||||
}
|
||||
|
||||
// Format if needed
|
||||
const formattedValue = internalFormatter(nextCells.map((c) => c || ' ').join(''));
|
||||
nextCells = strToArr(formattedValue).map((c, i) => {
|
||||
if (c === ' ' && !nextCells[i]) {
|
||||
return nextCells[i];
|
||||
}
|
||||
return c;
|
||||
});
|
||||
|
||||
return nextCells;
|
||||
});
|
||||
|
||||
// ======================== Change ========================
|
||||
const onInputChange: OTPInputProps['onChange'] = (index, txt) => {
|
||||
const nextCells = patchValue(index, txt);
|
||||
|
||||
const nextIndex = Math.min(index + txt.length, length - 1);
|
||||
if (nextIndex !== index) {
|
||||
refs.current[nextIndex]?.focus();
|
||||
}
|
||||
|
||||
triggerValueCellsChange(nextCells);
|
||||
};
|
||||
|
||||
const onInputActiveChange: OTPInputProps['onActiveChange'] = (nextIndex) => {
|
||||
refs.current[nextIndex]?.focus();
|
||||
};
|
||||
|
||||
// ======================== Render ========================
|
||||
const inputSharedProps = {
|
||||
variant,
|
||||
disabled,
|
||||
status: mergedStatus as InputStatus,
|
||||
};
|
||||
|
||||
return wrapCSSVar(
|
||||
<div
|
||||
{...domAttrs}
|
||||
ref={containerRef}
|
||||
className={classNames(
|
||||
prefixCls,
|
||||
{
|
||||
[`${prefixCls}-sm`]: mergedSize === 'small',
|
||||
[`${prefixCls}-lg`]: mergedSize === 'large',
|
||||
[`${prefixCls}-rtl`]: direction === 'rtl',
|
||||
},
|
||||
cssVarCls,
|
||||
hashId,
|
||||
)}
|
||||
>
|
||||
<FormItemInputContext.Provider value={proxyFormContext}>
|
||||
{new Array(length).fill(0).map((_, index) => {
|
||||
const key = `otp-${index}`;
|
||||
const singleValue = valueCells[index] || '';
|
||||
|
||||
return (
|
||||
<OTPInput
|
||||
ref={(inputEle) => {
|
||||
refs.current[index] = inputEle;
|
||||
}}
|
||||
key={key}
|
||||
index={index}
|
||||
size={mergedSize}
|
||||
htmlSize={1}
|
||||
className={`${prefixCls}-input`}
|
||||
onChange={onInputChange}
|
||||
value={singleValue}
|
||||
onActiveChange={onInputActiveChange}
|
||||
autoFocus={index === 0 && autoFocus}
|
||||
{...inputSharedProps}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</FormItemInputContext.Provider>
|
||||
</div>,
|
||||
);
|
||||
});
|
||||
|
||||
export default OTP;
|
@ -10114,6 +10114,245 @@ exports[`renders components/input/demo/group.tsx extend context correctly 2`] =
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`renders components/input/demo/otp.tsx extend context correctly 1`] = `
|
||||
<div
|
||||
class="ant-space ant-space-vertical ant-space-gap-row-small ant-space-gap-col-small"
|
||||
>
|
||||
<div
|
||||
class="ant-space-item"
|
||||
>
|
||||
<h5
|
||||
class="ant-typography"
|
||||
>
|
||||
With formatter (Upcase)
|
||||
</h5>
|
||||
</div>
|
||||
<div
|
||||
class="ant-space-item"
|
||||
>
|
||||
<div
|
||||
class="ant-otp"
|
||||
>
|
||||
<input
|
||||
class="ant-input ant-input-outlined ant-otp-input"
|
||||
size="1"
|
||||
type="text"
|
||||
value=""
|
||||
/>
|
||||
<input
|
||||
class="ant-input ant-input-outlined ant-otp-input"
|
||||
size="1"
|
||||
type="text"
|
||||
value=""
|
||||
/>
|
||||
<input
|
||||
class="ant-input ant-input-outlined ant-otp-input"
|
||||
size="1"
|
||||
type="text"
|
||||
value=""
|
||||
/>
|
||||
<input
|
||||
class="ant-input ant-input-outlined ant-otp-input"
|
||||
size="1"
|
||||
type="text"
|
||||
value=""
|
||||
/>
|
||||
<input
|
||||
class="ant-input ant-input-outlined ant-otp-input"
|
||||
size="1"
|
||||
type="text"
|
||||
value=""
|
||||
/>
|
||||
<input
|
||||
class="ant-input ant-input-outlined ant-otp-input"
|
||||
size="1"
|
||||
type="text"
|
||||
value=""
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="ant-space-item"
|
||||
>
|
||||
<h5
|
||||
class="ant-typography"
|
||||
>
|
||||
With Disabled
|
||||
</h5>
|
||||
</div>
|
||||
<div
|
||||
class="ant-space-item"
|
||||
>
|
||||
<div
|
||||
class="ant-otp"
|
||||
>
|
||||
<input
|
||||
class="ant-input ant-input-disabled ant-input-outlined ant-otp-input"
|
||||
disabled=""
|
||||
size="1"
|
||||
type="text"
|
||||
value=""
|
||||
/>
|
||||
<input
|
||||
class="ant-input ant-input-disabled ant-input-outlined ant-otp-input"
|
||||
disabled=""
|
||||
size="1"
|
||||
type="text"
|
||||
value=""
|
||||
/>
|
||||
<input
|
||||
class="ant-input ant-input-disabled ant-input-outlined ant-otp-input"
|
||||
disabled=""
|
||||
size="1"
|
||||
type="text"
|
||||
value=""
|
||||
/>
|
||||
<input
|
||||
class="ant-input ant-input-disabled ant-input-outlined ant-otp-input"
|
||||
disabled=""
|
||||
size="1"
|
||||
type="text"
|
||||
value=""
|
||||
/>
|
||||
<input
|
||||
class="ant-input ant-input-disabled ant-input-outlined ant-otp-input"
|
||||
disabled=""
|
||||
size="1"
|
||||
type="text"
|
||||
value=""
|
||||
/>
|
||||
<input
|
||||
class="ant-input ant-input-disabled ant-input-outlined ant-otp-input"
|
||||
disabled=""
|
||||
size="1"
|
||||
type="text"
|
||||
value=""
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="ant-space-item"
|
||||
>
|
||||
<h5
|
||||
class="ant-typography"
|
||||
>
|
||||
With Length (8)
|
||||
</h5>
|
||||
</div>
|
||||
<div
|
||||
class="ant-space-item"
|
||||
>
|
||||
<div
|
||||
class="ant-otp"
|
||||
>
|
||||
<input
|
||||
class="ant-input ant-input-outlined ant-otp-input"
|
||||
size="1"
|
||||
type="text"
|
||||
value=""
|
||||
/>
|
||||
<input
|
||||
class="ant-input ant-input-outlined ant-otp-input"
|
||||
size="1"
|
||||
type="text"
|
||||
value=""
|
||||
/>
|
||||
<input
|
||||
class="ant-input ant-input-outlined ant-otp-input"
|
||||
size="1"
|
||||
type="text"
|
||||
value=""
|
||||
/>
|
||||
<input
|
||||
class="ant-input ant-input-outlined ant-otp-input"
|
||||
size="1"
|
||||
type="text"
|
||||
value=""
|
||||
/>
|
||||
<input
|
||||
class="ant-input ant-input-outlined ant-otp-input"
|
||||
size="1"
|
||||
type="text"
|
||||
value=""
|
||||
/>
|
||||
<input
|
||||
class="ant-input ant-input-outlined ant-otp-input"
|
||||
size="1"
|
||||
type="text"
|
||||
value=""
|
||||
/>
|
||||
<input
|
||||
class="ant-input ant-input-outlined ant-otp-input"
|
||||
size="1"
|
||||
type="text"
|
||||
value=""
|
||||
/>
|
||||
<input
|
||||
class="ant-input ant-input-outlined ant-otp-input"
|
||||
size="1"
|
||||
type="text"
|
||||
value=""
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="ant-space-item"
|
||||
>
|
||||
<h5
|
||||
class="ant-typography"
|
||||
>
|
||||
With variant
|
||||
</h5>
|
||||
</div>
|
||||
<div
|
||||
class="ant-space-item"
|
||||
>
|
||||
<div
|
||||
class="ant-otp"
|
||||
>
|
||||
<input
|
||||
class="ant-input ant-input-filled ant-otp-input"
|
||||
size="1"
|
||||
type="text"
|
||||
value=""
|
||||
/>
|
||||
<input
|
||||
class="ant-input ant-input-filled ant-otp-input"
|
||||
size="1"
|
||||
type="text"
|
||||
value=""
|
||||
/>
|
||||
<input
|
||||
class="ant-input ant-input-filled ant-otp-input"
|
||||
size="1"
|
||||
type="text"
|
||||
value=""
|
||||
/>
|
||||
<input
|
||||
class="ant-input ant-input-filled ant-otp-input"
|
||||
size="1"
|
||||
type="text"
|
||||
value=""
|
||||
/>
|
||||
<input
|
||||
class="ant-input ant-input-filled ant-otp-input"
|
||||
size="1"
|
||||
type="text"
|
||||
value=""
|
||||
/>
|
||||
<input
|
||||
class="ant-input ant-input-filled ant-otp-input"
|
||||
size="1"
|
||||
type="text"
|
||||
value=""
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`renders components/input/demo/otp.tsx extend context correctly 2`] = `[]`;
|
||||
|
||||
exports[`renders components/input/demo/password-input.tsx extend context correctly 1`] = `
|
||||
<div
|
||||
class="ant-space ant-space-vertical ant-space-gap-row-small ant-space-gap-col-small"
|
||||
|
@ -3511,6 +3511,243 @@ exports[`renders components/input/demo/group.tsx correctly 1`] = `
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`renders components/input/demo/otp.tsx correctly 1`] = `
|
||||
<div
|
||||
class="ant-space ant-space-vertical ant-space-gap-row-small ant-space-gap-col-small"
|
||||
>
|
||||
<div
|
||||
class="ant-space-item"
|
||||
>
|
||||
<h5
|
||||
class="ant-typography"
|
||||
>
|
||||
With formatter (Upcase)
|
||||
</h5>
|
||||
</div>
|
||||
<div
|
||||
class="ant-space-item"
|
||||
>
|
||||
<div
|
||||
class="ant-otp"
|
||||
>
|
||||
<input
|
||||
class="ant-input ant-input-outlined ant-otp-input"
|
||||
size="1"
|
||||
type="text"
|
||||
value=""
|
||||
/>
|
||||
<input
|
||||
class="ant-input ant-input-outlined ant-otp-input"
|
||||
size="1"
|
||||
type="text"
|
||||
value=""
|
||||
/>
|
||||
<input
|
||||
class="ant-input ant-input-outlined ant-otp-input"
|
||||
size="1"
|
||||
type="text"
|
||||
value=""
|
||||
/>
|
||||
<input
|
||||
class="ant-input ant-input-outlined ant-otp-input"
|
||||
size="1"
|
||||
type="text"
|
||||
value=""
|
||||
/>
|
||||
<input
|
||||
class="ant-input ant-input-outlined ant-otp-input"
|
||||
size="1"
|
||||
type="text"
|
||||
value=""
|
||||
/>
|
||||
<input
|
||||
class="ant-input ant-input-outlined ant-otp-input"
|
||||
size="1"
|
||||
type="text"
|
||||
value=""
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="ant-space-item"
|
||||
>
|
||||
<h5
|
||||
class="ant-typography"
|
||||
>
|
||||
With Disabled
|
||||
</h5>
|
||||
</div>
|
||||
<div
|
||||
class="ant-space-item"
|
||||
>
|
||||
<div
|
||||
class="ant-otp"
|
||||
>
|
||||
<input
|
||||
class="ant-input ant-input-disabled ant-input-outlined ant-otp-input"
|
||||
disabled=""
|
||||
size="1"
|
||||
type="text"
|
||||
value=""
|
||||
/>
|
||||
<input
|
||||
class="ant-input ant-input-disabled ant-input-outlined ant-otp-input"
|
||||
disabled=""
|
||||
size="1"
|
||||
type="text"
|
||||
value=""
|
||||
/>
|
||||
<input
|
||||
class="ant-input ant-input-disabled ant-input-outlined ant-otp-input"
|
||||
disabled=""
|
||||
size="1"
|
||||
type="text"
|
||||
value=""
|
||||
/>
|
||||
<input
|
||||
class="ant-input ant-input-disabled ant-input-outlined ant-otp-input"
|
||||
disabled=""
|
||||
size="1"
|
||||
type="text"
|
||||
value=""
|
||||
/>
|
||||
<input
|
||||
class="ant-input ant-input-disabled ant-input-outlined ant-otp-input"
|
||||
disabled=""
|
||||
size="1"
|
||||
type="text"
|
||||
value=""
|
||||
/>
|
||||
<input
|
||||
class="ant-input ant-input-disabled ant-input-outlined ant-otp-input"
|
||||
disabled=""
|
||||
size="1"
|
||||
type="text"
|
||||
value=""
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="ant-space-item"
|
||||
>
|
||||
<h5
|
||||
class="ant-typography"
|
||||
>
|
||||
With Length (8)
|
||||
</h5>
|
||||
</div>
|
||||
<div
|
||||
class="ant-space-item"
|
||||
>
|
||||
<div
|
||||
class="ant-otp"
|
||||
>
|
||||
<input
|
||||
class="ant-input ant-input-outlined ant-otp-input"
|
||||
size="1"
|
||||
type="text"
|
||||
value=""
|
||||
/>
|
||||
<input
|
||||
class="ant-input ant-input-outlined ant-otp-input"
|
||||
size="1"
|
||||
type="text"
|
||||
value=""
|
||||
/>
|
||||
<input
|
||||
class="ant-input ant-input-outlined ant-otp-input"
|
||||
size="1"
|
||||
type="text"
|
||||
value=""
|
||||
/>
|
||||
<input
|
||||
class="ant-input ant-input-outlined ant-otp-input"
|
||||
size="1"
|
||||
type="text"
|
||||
value=""
|
||||
/>
|
||||
<input
|
||||
class="ant-input ant-input-outlined ant-otp-input"
|
||||
size="1"
|
||||
type="text"
|
||||
value=""
|
||||
/>
|
||||
<input
|
||||
class="ant-input ant-input-outlined ant-otp-input"
|
||||
size="1"
|
||||
type="text"
|
||||
value=""
|
||||
/>
|
||||
<input
|
||||
class="ant-input ant-input-outlined ant-otp-input"
|
||||
size="1"
|
||||
type="text"
|
||||
value=""
|
||||
/>
|
||||
<input
|
||||
class="ant-input ant-input-outlined ant-otp-input"
|
||||
size="1"
|
||||
type="text"
|
||||
value=""
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="ant-space-item"
|
||||
>
|
||||
<h5
|
||||
class="ant-typography"
|
||||
>
|
||||
With variant
|
||||
</h5>
|
||||
</div>
|
||||
<div
|
||||
class="ant-space-item"
|
||||
>
|
||||
<div
|
||||
class="ant-otp"
|
||||
>
|
||||
<input
|
||||
class="ant-input ant-input-filled ant-otp-input"
|
||||
size="1"
|
||||
type="text"
|
||||
value=""
|
||||
/>
|
||||
<input
|
||||
class="ant-input ant-input-filled ant-otp-input"
|
||||
size="1"
|
||||
type="text"
|
||||
value=""
|
||||
/>
|
||||
<input
|
||||
class="ant-input ant-input-filled ant-otp-input"
|
||||
size="1"
|
||||
type="text"
|
||||
value=""
|
||||
/>
|
||||
<input
|
||||
class="ant-input ant-input-filled ant-otp-input"
|
||||
size="1"
|
||||
type="text"
|
||||
value=""
|
||||
/>
|
||||
<input
|
||||
class="ant-input ant-input-filled ant-otp-input"
|
||||
size="1"
|
||||
type="text"
|
||||
value=""
|
||||
/>
|
||||
<input
|
||||
class="ant-input ant-input-filled ant-otp-input"
|
||||
size="1"
|
||||
type="text"
|
||||
value=""
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`renders components/input/demo/password-input.tsx correctly 1`] = `
|
||||
<div
|
||||
class="ant-space ant-space-vertical ant-space-gap-row-small ant-space-gap-col-small"
|
||||
|
44
components/input/__tests__/__snapshots__/otp.test.tsx.snap
Normal file
44
components/input/__tests__/__snapshots__/otp.test.tsx.snap
Normal file
@ -0,0 +1,44 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`Input.OTP rtl render component should be rendered correctly in RTL direction 1`] = `
|
||||
<div
|
||||
class="ant-otp ant-otp-rtl"
|
||||
>
|
||||
<input
|
||||
class="ant-input ant-input-rtl ant-input-outlined ant-otp-input"
|
||||
size="1"
|
||||
type="text"
|
||||
value=""
|
||||
/>
|
||||
<input
|
||||
class="ant-input ant-input-rtl ant-input-outlined ant-otp-input"
|
||||
size="1"
|
||||
type="text"
|
||||
value=""
|
||||
/>
|
||||
<input
|
||||
class="ant-input ant-input-rtl ant-input-outlined ant-otp-input"
|
||||
size="1"
|
||||
type="text"
|
||||
value=""
|
||||
/>
|
||||
<input
|
||||
class="ant-input ant-input-rtl ant-input-outlined ant-otp-input"
|
||||
size="1"
|
||||
type="text"
|
||||
value=""
|
||||
/>
|
||||
<input
|
||||
class="ant-input ant-input-rtl ant-input-outlined ant-otp-input"
|
||||
size="1"
|
||||
type="text"
|
||||
value=""
|
||||
/>
|
||||
<input
|
||||
class="ant-input ant-input-rtl ant-input-outlined ant-otp-input"
|
||||
size="1"
|
||||
type="text"
|
||||
value=""
|
||||
/>
|
||||
</div>
|
||||
`;
|
131
components/input/__tests__/otp.test.tsx
Normal file
131
components/input/__tests__/otp.test.tsx
Normal file
@ -0,0 +1,131 @@
|
||||
import React from 'react';
|
||||
|
||||
import Input from '..';
|
||||
import focusTest from '../../../tests/shared/focusTest';
|
||||
import mountTest from '../../../tests/shared/mountTest';
|
||||
import rtlTest from '../../../tests/shared/rtlTest';
|
||||
import { fireEvent, render, waitFakeTimer } from '../../../tests/utils';
|
||||
|
||||
const { OTP } = Input;
|
||||
|
||||
describe('Input.OTP', () => {
|
||||
focusTest(Input.OTP, { refFocus: true });
|
||||
mountTest(Input.OTP);
|
||||
rtlTest(Input.OTP);
|
||||
|
||||
function getText(container: HTMLElement) {
|
||||
const inputList = container.querySelectorAll('input');
|
||||
return Array.from(inputList)
|
||||
.map((input) => input.value || ' ')
|
||||
.join('')
|
||||
.replace(/\s*$/, '');
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
jest.useFakeTimers();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllTimers();
|
||||
jest.useRealTimers();
|
||||
});
|
||||
|
||||
it('paste to fill all', async () => {
|
||||
const onChange = jest.fn();
|
||||
const { container } = render(<OTP onChange={onChange} />);
|
||||
|
||||
fireEvent.input(container.querySelector('input')!, { target: { value: '123456' } });
|
||||
expect(onChange).toHaveBeenCalledTimes(1);
|
||||
expect(onChange).toHaveBeenCalledWith('123456');
|
||||
});
|
||||
|
||||
it('fill step by step', () => {
|
||||
const CODE = 'BAMBOO';
|
||||
const onChange = jest.fn();
|
||||
render(<OTP onChange={onChange} autoFocus />);
|
||||
|
||||
for (let i = 0; i < CODE.length; i += 1) {
|
||||
expect(onChange).not.toHaveBeenCalled();
|
||||
fireEvent.input(document.activeElement!, { target: { value: CODE[i] } });
|
||||
}
|
||||
|
||||
expect(onChange).toHaveBeenCalledTimes(1);
|
||||
expect(onChange).toHaveBeenCalledWith(CODE);
|
||||
});
|
||||
|
||||
it('backspace to delete', async () => {
|
||||
const CODE = 'LITTLE';
|
||||
|
||||
const onChange = jest.fn();
|
||||
const { container } = render(<OTP defaultValue={CODE} onChange={onChange} />);
|
||||
expect(getText(container)).toBe(CODE);
|
||||
|
||||
// Focus on the last cell
|
||||
const inputList = container.querySelectorAll('input');
|
||||
inputList[inputList.length - 1].focus();
|
||||
|
||||
for (let i = 0; i < CODE.length; i += 1) {
|
||||
fireEvent.keyDown(document.activeElement!, { key: 'Backspace' });
|
||||
fireEvent.input(document.activeElement!, { target: { value: '' } });
|
||||
fireEvent.keyUp(document.activeElement!, { key: 'Backspace' });
|
||||
}
|
||||
|
||||
expect(getText(container)).toBe('');
|
||||
|
||||
// We do not trigger change if empty. It's safe to modify this logic if needed.
|
||||
expect(onChange).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('controlled', () => {
|
||||
const { container, rerender } = render(<OTP value="BAMBOO" />);
|
||||
expect(getText(container)).toBe('BAMBOO');
|
||||
|
||||
rerender(<OTP value="LITTLE" />);
|
||||
expect(getText(container)).toBe('LITTLE');
|
||||
});
|
||||
|
||||
it('focus to selection', async () => {
|
||||
const { container } = render(<OTP defaultValue="BAMBOO" />);
|
||||
|
||||
const firstInput = container.querySelector('input')!;
|
||||
const selectSpy = jest.spyOn(firstInput, 'select');
|
||||
expect(selectSpy).not.toHaveBeenCalled();
|
||||
|
||||
// Trigger focus
|
||||
firstInput.focus();
|
||||
await waitFakeTimer();
|
||||
|
||||
expect(selectSpy).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('arrow key to switch', () => {
|
||||
const { container } = render(<OTP autoFocus />);
|
||||
|
||||
const inputList = Array.from(container.querySelectorAll('input'));
|
||||
expect(document.activeElement).toEqual(inputList[0]);
|
||||
|
||||
fireEvent.keyDown(document.activeElement!, { key: 'ArrowRight' });
|
||||
expect(document.activeElement).toEqual(inputList[1]);
|
||||
|
||||
fireEvent.keyDown(document.activeElement!, { key: 'ArrowLeft' });
|
||||
expect(document.activeElement).toEqual(inputList[0]);
|
||||
});
|
||||
|
||||
it('fill last cell', () => {
|
||||
const { container } = render(<OTP />);
|
||||
fireEvent.input(container.querySelectorAll('input')[5], { target: { value: '1' } });
|
||||
|
||||
expect(getText(container)).toBe(' 1');
|
||||
});
|
||||
|
||||
it('formatter', () => {
|
||||
const { container } = render(
|
||||
<OTP defaultValue="bamboo" formatter={(val) => val.toUpperCase()} />,
|
||||
);
|
||||
expect(getText(container)).toBe('BAMBOO');
|
||||
|
||||
// Type to trigger formatter
|
||||
fireEvent.input(container.querySelector('input')!, { target: { value: 'little' } });
|
||||
expect(getText(container)).toBe('LITTLE');
|
||||
});
|
||||
});
|
7
components/input/demo/otp.md
Normal file
7
components/input/demo/otp.md
Normal file
@ -0,0 +1,7 @@
|
||||
## zh-CN
|
||||
|
||||
一次性密码输入框。
|
||||
|
||||
## en-US
|
||||
|
||||
One time password input.
|
29
components/input/demo/otp.tsx
Normal file
29
components/input/demo/otp.tsx
Normal file
@ -0,0 +1,29 @@
|
||||
import React from 'react';
|
||||
import { Input, Space, Typography, type GetProp } from 'antd';
|
||||
|
||||
const { Title } = Typography;
|
||||
|
||||
const App: React.FC = () => {
|
||||
const onChange: GetProp<typeof Input.OTP, 'onChange'> = (text) => {
|
||||
console.log('onChange:', text);
|
||||
};
|
||||
|
||||
const sharedProps = {
|
||||
onChange,
|
||||
};
|
||||
|
||||
return (
|
||||
<Space direction="vertical">
|
||||
<Title level={5}>With formatter (Upcase)</Title>
|
||||
<Input.OTP formatter={(str) => str.toUpperCase()} {...sharedProps} />
|
||||
<Title level={5}>With Disabled</Title>
|
||||
<Input.OTP disabled {...sharedProps} />
|
||||
<Title level={5}>With Length (8)</Title>
|
||||
<Input.OTP length={8} {...sharedProps} />
|
||||
<Title level={5}>With variant</Title>
|
||||
<Input.OTP variant="filled" {...sharedProps} />
|
||||
</Space>
|
||||
);
|
||||
};
|
||||
|
||||
export default App;
|
@ -28,6 +28,7 @@ demo:
|
||||
<code src="./demo/search-input-loading.tsx">Search box with loading</code>
|
||||
<code src="./demo/textarea.tsx">TextArea</code>
|
||||
<code src="./demo/autosize-textarea.tsx">Autosizing the height to fit the content</code>
|
||||
<code src="./demo/otp.tsx" version="5.16.0">OTP</code>
|
||||
<code src="./demo/tooltip.tsx">Format Tooltip Input</code>
|
||||
<code src="./demo/presuffix.tsx">prefix and suffix</code>
|
||||
<code src="./demo/password-input.tsx">Password box</code>
|
||||
@ -102,7 +103,7 @@ Same as Input, and more:
|
||||
|
||||
The rest of the props of `Input.TextArea` are the same as the original [textarea](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/textarea).
|
||||
|
||||
#### Input.Search
|
||||
### Input.Search
|
||||
|
||||
| Property | Description | Type | Default |
|
||||
| --- | --- | --- | --- |
|
||||
@ -112,13 +113,29 @@ The rest of the props of `Input.TextArea` are the same as the original [textarea
|
||||
|
||||
Supports all props of `Input`.
|
||||
|
||||
#### Input.Password
|
||||
### Input.Password
|
||||
|
||||
| Property | Description | Type | Default | Version |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| iconRender | Custom toggle button | (visible) => ReactNode | (visible) => (visible ? <EyeOutlined /> : <EyeInvisibleOutlined />) | 4.3.0 |
|
||||
| visibilityToggle | Whether show toggle button or control password visible | boolean \| [VisibilityToggle](#visibilitytoggle) | true | |
|
||||
|
||||
### Input.OTP
|
||||
|
||||
Added in `5.16.0`.
|
||||
|
||||
| Property | Description | Type | Default | Version |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| defaultValue | Default value | string | - | |
|
||||
| disabled | Whether the input is disabled | boolean | false | |
|
||||
| formatter | Format display, blank fields will be filled with ` ` | (value: string) => string | - | |
|
||||
| length | The number of input elements | number | 6 | |
|
||||
| status | Set validation status | 'error' \| 'warning' | - | |
|
||||
| size | The size of the input box | `small` \| `middle` \| `large` | `middle` | |
|
||||
| variant | Variants of Input | `outlined` \| `borderless` \| `filled` | `outlined` | |
|
||||
| value | The input content value | string | - | |
|
||||
| onChange | Trigger when all the fields are filled | function(value: string) | - | |
|
||||
|
||||
#### VisibilityToggle
|
||||
|
||||
| Property | Description | Type | Default | Version |
|
||||
|
@ -1,7 +1,9 @@
|
||||
import type * as React from 'react';
|
||||
|
||||
import Group from './Group';
|
||||
import type { InputProps, InputRef } from './Input';
|
||||
import InternalInput from './Input';
|
||||
import OTP from './OTP';
|
||||
import Password from './Password';
|
||||
import Search from './Search';
|
||||
import TextArea from './TextArea';
|
||||
@ -19,16 +21,14 @@ type CompoundedComponent = React.ForwardRefExoticComponent<
|
||||
Search: typeof Search;
|
||||
TextArea: typeof TextArea;
|
||||
Password: typeof Password;
|
||||
OTP: typeof OTP;
|
||||
};
|
||||
|
||||
const Input = InternalInput as CompoundedComponent;
|
||||
|
||||
if (process.env.NODE_ENV !== 'production') {
|
||||
Input.displayName = 'Input';
|
||||
}
|
||||
|
||||
Input.Group = Group;
|
||||
Input.Search = Search;
|
||||
Input.TextArea = TextArea;
|
||||
Input.Password = Password;
|
||||
Input.OTP = OTP;
|
||||
export default Input;
|
||||
|
@ -29,6 +29,7 @@ demo:
|
||||
<code src="./demo/search-input-loading.tsx">搜索框 loading</code>
|
||||
<code src="./demo/textarea.tsx">文本域</code>
|
||||
<code src="./demo/autosize-textarea.tsx">适应文本高度的文本域</code>
|
||||
<code src="./demo/otp.tsx" version="5.16.0">一次性密码框</code>
|
||||
<code src="./demo/tooltip.tsx">输入时格式化展示</code>
|
||||
<code src="./demo/presuffix.tsx">前缀和后缀</code>
|
||||
<code src="./demo/password-input.tsx">密码框</code>
|
||||
@ -103,7 +104,7 @@ interface CountConfig {
|
||||
|
||||
`Input.TextArea` 的其他属性和浏览器自带的 [textarea](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/textarea) 一致。
|
||||
|
||||
#### Input.Search
|
||||
### Input.Search
|
||||
|
||||
| 参数 | 说明 | 类型 | 默认值 |
|
||||
| --- | --- | --- | --- |
|
||||
@ -113,13 +114,29 @@ interface CountConfig {
|
||||
|
||||
其余属性和 Input 一致。
|
||||
|
||||
#### Input.Password
|
||||
### Input.Password
|
||||
|
||||
| 参数 | 说明 | 类型 | 默认值 | 版本 |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| iconRender | 自定义切换按钮 | (visible) => ReactNode | (visible) => (visible ? <EyeOutlined /> : <EyeInvisibleOutlined />) | 4.3.0 |
|
||||
| visibilityToggle | 是否显示切换按钮或者控制密码显隐 | boolean \| [VisibilityToggle](#visibilitytoggle) | true | |
|
||||
|
||||
### Input.OTP
|
||||
|
||||
`5.16.0` 新增。
|
||||
|
||||
| 参数 | 说明 | 类型 | 默认值 | 版本 |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| defaultValue | 默认值 | string | - | |
|
||||
| disabled | 是否禁用 | boolean | false | |
|
||||
| formatter | 格式化展示,留空字段会被 ` ` 填充 | (value: string) => string | - | |
|
||||
| length | 输入元素数量 | number | 6 | |
|
||||
| status | 设置校验状态 | 'error' \| 'warning' | - | |
|
||||
| size | 输入框大小 | `small` \| `middle` \| `large` | `middle` | |
|
||||
| variant | 形态变体 | `outlined` \| `borderless` \| `filled` | `outlined` | |
|
||||
| value | 输入框内容 | string | - | |
|
||||
| onChange | 当输入框内容全部填充时触发回调 | function(value: string) | - | |
|
||||
|
||||
#### VisibilityToggle
|
||||
|
||||
| Property | Description | Type | Default | Version |
|
||||
|
47
components/input/style/otp.ts
Normal file
47
components/input/style/otp.ts
Normal file
@ -0,0 +1,47 @@
|
||||
import type { GenerateStyle } from '../../theme/internal';
|
||||
import { genStyleHooks, mergeToken } from '../../theme/internal';
|
||||
import type { InputToken } from './token';
|
||||
import { initComponentToken, initInputToken } from './token';
|
||||
|
||||
// =============================== OTP ================================
|
||||
const genOTPStyle: GenerateStyle<InputToken> = (token) => {
|
||||
const { componentCls, paddingXS } = token;
|
||||
|
||||
return {
|
||||
[`${componentCls}`]: {
|
||||
display: 'inline-flex',
|
||||
alignItems: 'center',
|
||||
flexWrap: 'nowrap',
|
||||
columnGap: paddingXS,
|
||||
|
||||
'&-rtl': {
|
||||
direction: 'rtl',
|
||||
},
|
||||
|
||||
[`${componentCls}-input`]: {
|
||||
textAlign: 'center',
|
||||
paddingInline: token.paddingXXS,
|
||||
},
|
||||
|
||||
// ================= Size =================
|
||||
[`&${componentCls}-sm ${componentCls}-input`]: {
|
||||
paddingInline: token.calc(token.paddingXXS).div(2).equal(),
|
||||
},
|
||||
|
||||
[`&${componentCls}-lg ${componentCls}-input`]: {
|
||||
paddingInline: token.paddingXS,
|
||||
},
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
// ============================== Export ==============================
|
||||
export default genStyleHooks(
|
||||
['Input', 'OTP'],
|
||||
(token) => {
|
||||
const inputToken = mergeToken<InputToken>(token, initInputToken(token));
|
||||
|
||||
return [genOTPStyle(inputToken)];
|
||||
},
|
||||
initComponentToken,
|
||||
);
|
File diff suppressed because it is too large
Load Diff
@ -61,6 +61,7 @@ import 'dayjs/locale/tr';
|
||||
import 'dayjs/locale/uk';
|
||||
import 'dayjs/locale/ur';
|
||||
import 'dayjs/locale/vi';
|
||||
import 'dayjs/locale/uz-latn';
|
||||
import 'dayjs/locale/zh-cn';
|
||||
import 'dayjs/locale/zh-hk';
|
||||
import 'dayjs/locale/zh-tw';
|
||||
@ -151,6 +152,7 @@ import zhCN from '../../locale/zh_CN';
|
||||
import zhHK from '../../locale/zh_HK';
|
||||
import zhTW from '../../locale/zh_TW';
|
||||
import myMM from '../../locale/my_MM';
|
||||
import uzUZ from '../../locale/uz_UZ';
|
||||
|
||||
dayjs.extend(preParsePostFormat);
|
||||
|
||||
@ -224,6 +226,7 @@ const locales = [
|
||||
zhTW,
|
||||
urPK,
|
||||
myMM,
|
||||
uzUZ,
|
||||
];
|
||||
|
||||
const { Option } = Select;
|
||||
|
@ -80,6 +80,7 @@ const localeValues: Locale = {
|
||||
copy: 'Copy',
|
||||
copied: 'Copied',
|
||||
expand: 'Expand',
|
||||
collapse: 'Collapse',
|
||||
},
|
||||
Form: {
|
||||
optional: '(optional)',
|
||||
|
@ -40,6 +40,7 @@ export interface Locale {
|
||||
copy?: any;
|
||||
copied?: any;
|
||||
expand?: any;
|
||||
collapse?: any;
|
||||
};
|
||||
Form?: {
|
||||
optional?: string;
|
||||
|
149
components/locale/uz_UZ.ts
Normal file
149
components/locale/uz_UZ.ts
Normal file
@ -0,0 +1,149 @@
|
||||
/* eslint-disable no-template-curly-in-string */
|
||||
|
||||
import Pagination from 'rc-pagination/lib/locale/uz_UZ';
|
||||
|
||||
import type { Locale } from '.';
|
||||
import Calendar from '../calendar/locale/uz_UZ';
|
||||
import DatePicker from '../date-picker/locale/uz_UZ';
|
||||
import TimePicker from '../time-picker/locale/uz_UZ';
|
||||
|
||||
const typeTemplate: string = '${label} ${type} turi emas';
|
||||
|
||||
const localeValues: Locale = {
|
||||
// NOTE: In
|
||||
// https://github.com/react-component/picker/blob/master/src/locale/uz_UZ.ts
|
||||
// and
|
||||
// https://github.com/react-component/pagination/blob/master/src/locale/uz_UZ.ts
|
||||
// both implemented as uz-latn Uzbek
|
||||
locale: 'uz-latn',
|
||||
Pagination,
|
||||
DatePicker,
|
||||
TimePicker,
|
||||
Calendar,
|
||||
global: {
|
||||
placeholder: 'Iltimos tanlang',
|
||||
},
|
||||
Table: {
|
||||
filterTitle: 'Filtr',
|
||||
filterConfirm: 'OK',
|
||||
filterReset: 'Tshlash',
|
||||
filterEmptyText: 'Filtrlarsiz',
|
||||
filterCheckall: 'Barcha elementlarni tanlash',
|
||||
filterSearchPlaceholder: 'Filtrlarda qidiruv',
|
||||
emptyText: "Ma'lumotlar topilmadi",
|
||||
selectAll: 'Barchasini tanlash',
|
||||
selectInvert: 'Tanlovni aylantirish',
|
||||
selectNone: "Barcha ma'lumotlarni tozalang",
|
||||
selectionAll: "Barcha ma'lumotlarni tanlash",
|
||||
sortTitle: 'Tartiblash',
|
||||
expand: 'Satirni yozish',
|
||||
collapse: "Satirni yig'ish",
|
||||
triggerDesc: 'Kamayish tartibida tartiblash uchun bosing',
|
||||
triggerAsc: "O'sish tartibida tartiblash uchun bosing",
|
||||
cancelSort: 'Tartiblshni rad etish uchun bosing',
|
||||
},
|
||||
Tour: {
|
||||
Next: "So'ngra",
|
||||
Previous: 'Ortga',
|
||||
Finish: 'Tugatish',
|
||||
},
|
||||
Modal: {
|
||||
okText: 'OK',
|
||||
cancelText: "O'chirish",
|
||||
justOkText: 'OK',
|
||||
},
|
||||
Popconfirm: {
|
||||
okText: 'OK',
|
||||
cancelText: 'Bekor qilish',
|
||||
},
|
||||
Transfer: {
|
||||
titles: ['', ''],
|
||||
searchPlaceholder: 'Qidiruv',
|
||||
itemUnit: 'элем.',
|
||||
itemsUnit: 'элем.',
|
||||
remove: 'Oʻchirish',
|
||||
selectAll: "Barch ma'lumotlarni tanlash",
|
||||
selectCurrent: 'Joriy sahifani tanlash',
|
||||
selectInvert: 'Tanlovni aylantirish',
|
||||
removeAll: "Barcha ma'lumotlarni o'chirish",
|
||||
removeCurrent: "Joriy sahifani o'chirish",
|
||||
},
|
||||
Upload: {
|
||||
uploading: 'Yuklanish...',
|
||||
removeFile: "Faylni o'chirish",
|
||||
uploadError: 'Yuklashda xatolik yuz berdi',
|
||||
previewFile: "Faylni oldindan ko'rish",
|
||||
downloadFile: 'Faylni yuklash',
|
||||
},
|
||||
Empty: {
|
||||
description: 'Maʼlumot topilmadi',
|
||||
},
|
||||
Icon: {
|
||||
icon: 'ikonka',
|
||||
},
|
||||
Text: {
|
||||
edit: 'Tahrirlash',
|
||||
copy: 'Nusxalash',
|
||||
copied: 'Nusxalandi',
|
||||
expand: 'Ochib qoyish',
|
||||
},
|
||||
Form: {
|
||||
optional: '(shart emas)',
|
||||
defaultValidateMessages: {
|
||||
default: '${label} maydonini tekshirishda xatolik yuz berdi',
|
||||
required: 'Iltimos, ${label} kiriting',
|
||||
enum: '${label}, [${enum}] dan biri boʻlishi kerak',
|
||||
whitespace: '${label} boʻsh boʻlishi mumkin emas',
|
||||
date: {
|
||||
format: '${label} toʻgʻri sana formatida emas',
|
||||
parse: '${label} sanaga aylantirilmaydi',
|
||||
invalid: "${label} tog'ri sana emas",
|
||||
},
|
||||
types: {
|
||||
string: typeTemplate,
|
||||
method: typeTemplate,
|
||||
array: typeTemplate,
|
||||
object: typeTemplate,
|
||||
number: typeTemplate,
|
||||
date: typeTemplate,
|
||||
boolean: typeTemplate,
|
||||
integer: typeTemplate,
|
||||
float: typeTemplate,
|
||||
regexp: typeTemplate,
|
||||
email: typeTemplate,
|
||||
url: typeTemplate,
|
||||
hex: typeTemplate,
|
||||
},
|
||||
string: {
|
||||
len: '${label}, ${len} ta belgidan iborat boʻlishi kerak',
|
||||
min: '${label} должна быть больше или равна ${min} символов',
|
||||
max: '${label}, ${max} belgidan katta yoki teng boʻlishi kerak',
|
||||
range: '${label} uzunligi ${min}-${max} belgilar orasida boʻlishi kerak',
|
||||
},
|
||||
number: {
|
||||
len: '${label}, ${len} ga teng boʻlishi kerak',
|
||||
min: '${label}, ${min} dan katta yoki teng boʻlishi kerak',
|
||||
max: '${label}, ${max} dan kichik yoki teng boʻlishi kerak',
|
||||
range: '${label}, ${min}-${max} orasida boʻlishi kerak',
|
||||
},
|
||||
array: {
|
||||
len: '${label} elementlari soni ${len} ga teng boʻlishi kerak',
|
||||
min: '${label} elementlari soni ${min} dan katta yoki teng boʻlishi kerak',
|
||||
max: '${label} elementlari soni ${max} dan kam yoki teng boʻlishi kerak',
|
||||
range: '${label} elementlari soni ${min} va ${max} orasida boʻlishi kerak',
|
||||
},
|
||||
pattern: {
|
||||
mismatch: '${label}, ${pattern} andazasiga mos emas',
|
||||
},
|
||||
},
|
||||
},
|
||||
Image: {
|
||||
preview: 'Ko‘rib chiqish',
|
||||
},
|
||||
QRCode: {
|
||||
expired: 'QR-kod eskirgan',
|
||||
refresh: 'Yangilash',
|
||||
},
|
||||
};
|
||||
|
||||
export default localeValues;
|
@ -80,6 +80,7 @@ const localeValues: Locale = {
|
||||
copy: '复制',
|
||||
copied: '复制成功',
|
||||
expand: '展开',
|
||||
collapse: '收起',
|
||||
},
|
||||
Form: {
|
||||
optional: '(可选)',
|
||||
|
@ -3,7 +3,7 @@ import CloseOutlined from '@ant-design/icons/CloseOutlined';
|
||||
import classNames from 'classnames';
|
||||
import Dialog from 'rc-dialog';
|
||||
|
||||
import useClosable from '../_util/hooks/useClosable';
|
||||
import useClosable, { pickClosable } from '../_util/hooks/useClosable';
|
||||
import { useZIndex } from '../_util/hooks/useZIndex';
|
||||
import { getTransitionName } from '../_util/motion';
|
||||
import { canUseDocElement } from '../_util/styleChecker';
|
||||
@ -44,7 +44,7 @@ const Modal: React.FC<ModalProps> = (props) => {
|
||||
getPopupContainer: getContextPopupContainer,
|
||||
getPrefixCls,
|
||||
direction,
|
||||
modal,
|
||||
modal: modalContext,
|
||||
} = React.useContext(ConfigContext);
|
||||
|
||||
const handleCancel = (e: React.MouseEvent<HTMLButtonElement>) => {
|
||||
@ -77,8 +77,6 @@ const Modal: React.FC<ModalProps> = (props) => {
|
||||
wrapClassName,
|
||||
centered,
|
||||
getContainer,
|
||||
closeIcon,
|
||||
closable,
|
||||
focusTriggerAfterClose = true,
|
||||
style,
|
||||
// Deprecated
|
||||
@ -106,13 +104,15 @@ const Modal: React.FC<ModalProps> = (props) => {
|
||||
<Footer {...props} onOk={handleOk} onCancel={handleCancel} />
|
||||
);
|
||||
|
||||
const [mergedClosable, mergedCloseIcon] = useClosable({
|
||||
closable,
|
||||
closeIcon: typeof closeIcon !== 'undefined' ? closeIcon : modal?.closeIcon,
|
||||
customCloseIconRender: (icon) => renderCloseIcon(prefixCls, icon),
|
||||
defaultCloseIcon: <CloseOutlined className={`${prefixCls}-close-icon`} />,
|
||||
defaultClosable: true,
|
||||
});
|
||||
const [mergedClosable, mergedCloseIcon] = useClosable(
|
||||
pickClosable(props),
|
||||
pickClosable(modalContext),
|
||||
{
|
||||
closable: true,
|
||||
closeIcon: <CloseOutlined className={`${prefixCls}-close-icon`} />,
|
||||
closeIconRender: (icon) => renderCloseIcon(prefixCls, icon),
|
||||
},
|
||||
);
|
||||
|
||||
// ============================ Refs ============================
|
||||
// Select `ant-modal-content` by `panelRef`
|
||||
@ -142,15 +142,15 @@ const Modal: React.FC<ModalProps> = (props) => {
|
||||
focusTriggerAfterClose={focusTriggerAfterClose}
|
||||
transitionName={getTransitionName(rootPrefixCls, 'zoom', props.transitionName)}
|
||||
maskTransitionName={getTransitionName(rootPrefixCls, 'fade', props.maskTransitionName)}
|
||||
className={classNames(hashId, className, modal?.className)}
|
||||
style={{ ...modal?.style, ...style }}
|
||||
className={classNames(hashId, className, modalContext?.className)}
|
||||
style={{ ...modalContext?.style, ...style }}
|
||||
classNames={{
|
||||
...modal?.classNames,
|
||||
...modalContext?.classNames,
|
||||
...modalClassNames,
|
||||
wrapper: classNames(wrapClassNameExtended, modalClassNames?.wrapper),
|
||||
}}
|
||||
styles={{
|
||||
...modal?.styles,
|
||||
...modalContext?.styles,
|
||||
...modalStyles,
|
||||
}}
|
||||
panelRef={panelRef}
|
||||
|
@ -3,6 +3,7 @@ import type { DialogProps } from 'rc-dialog';
|
||||
|
||||
import type { ButtonProps, LegacyButtonType } from '../button/button';
|
||||
import type { DirectionType } from '../config-provider';
|
||||
import type { ClosableType } from '../_util/hooks/useClosable';
|
||||
|
||||
export type ModalFooterRender = (
|
||||
originNode: React.ReactNode,
|
||||
@ -19,7 +20,7 @@ export interface ModalProps extends ModalCommonProps {
|
||||
/** The modal dialog's title */
|
||||
title?: React.ReactNode;
|
||||
/** Whether a close (x) button is visible on top right of the modal dialog or not. Recommend to use closeIcon instead. */
|
||||
closable?: boolean | ({ closeIcon?: React.ReactNode } & React.AriaAttributes);
|
||||
closable?: ClosableType;
|
||||
/** Specify a function that will be called when a user clicks the OK button */
|
||||
onOk?: (e: React.MouseEvent<HTMLButtonElement>) => void;
|
||||
/** Specify a function that will be called when a user clicks mask, close button on top right or Cancel button */
|
||||
@ -84,7 +85,7 @@ export interface ModalFuncProps extends ModalCommonProps {
|
||||
/** @deprecated Please use `open` instead. */
|
||||
visible?: boolean;
|
||||
title?: React.ReactNode;
|
||||
closable?: boolean | ({ closeIcon?: React.ReactNode } & React.AriaAttributes);
|
||||
closable?: ClosableType;
|
||||
content?: React.ReactNode;
|
||||
// TODO: find out exact types
|
||||
onOk?: (...args: any[]) => any;
|
||||
|
@ -506,6 +506,7 @@ exports[`renders components/notification/demo/render-panel.tsx extend context co
|
||||
</div>
|
||||
</div>
|
||||
<a
|
||||
aria-label="Close"
|
||||
class="ant-notification-notice-close"
|
||||
tabindex="0"
|
||||
>
|
||||
|
@ -494,6 +494,7 @@ exports[`renders components/notification/demo/render-panel.tsx correctly 1`] = `
|
||||
</div>
|
||||
</div>
|
||||
<a
|
||||
aria-label="Close"
|
||||
class="ant-notification-notice-close"
|
||||
tabindex="0"
|
||||
>
|
||||
|
@ -249,6 +249,42 @@ describe('notification', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('support config closable', async () => {
|
||||
notification.config({
|
||||
closable: {
|
||||
closeIcon: <span className="test-customize-icon" />,
|
||||
'aria-label': 'CloseBtn',
|
||||
},
|
||||
});
|
||||
|
||||
// Global Icon
|
||||
notification.open({
|
||||
message: 'Notification Title',
|
||||
duration: 0,
|
||||
});
|
||||
await awaitPromise();
|
||||
|
||||
expect(document.querySelector('.test-customize-icon')).toBeTruthy();
|
||||
expect(document.querySelector('*[aria-label="CloseBtn"]')).toBeTruthy();
|
||||
|
||||
// Notice Icon
|
||||
notification.open({
|
||||
message: 'Notification Title',
|
||||
duration: 0,
|
||||
closable: {
|
||||
closeIcon: <span className="replace-icon" />,
|
||||
'aria-label': 'CloseBtn2',
|
||||
},
|
||||
});
|
||||
|
||||
expect(document.querySelector('.replace-icon')).toBeTruthy();
|
||||
expect(document.querySelector('*[aria-label="CloseBtn2"]')).toBeTruthy();
|
||||
|
||||
notification.config({
|
||||
closable: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
it('closeIcon should be update', async () => {
|
||||
const list = ['1', '2'];
|
||||
list.forEach((type) => {
|
||||
|
@ -1,4 +1,5 @@
|
||||
import type * as React from 'react';
|
||||
import type { ClosableType } from '../_util/hooks/useClosable';
|
||||
|
||||
interface DivProps extends React.HTMLProps<HTMLDivElement> {
|
||||
'data-testid'?: string;
|
||||
@ -30,6 +31,7 @@ export interface ArgsProps {
|
||||
readonly type?: IconType;
|
||||
onClick?: () => void;
|
||||
closeIcon?: React.ReactNode;
|
||||
closable?: ClosableType;
|
||||
props?: DivProps;
|
||||
role?: 'alert' | 'status';
|
||||
}
|
||||
@ -53,6 +55,7 @@ export interface GlobalConfigProps {
|
||||
getContainer?: () => HTMLElement | ShadowRoot;
|
||||
placement?: NotificationPlacement;
|
||||
closeIcon?: React.ReactNode;
|
||||
closable?: ClosableType;
|
||||
rtl?: boolean;
|
||||
maxCount?: number;
|
||||
props?: DivProps;
|
||||
|
@ -148,6 +148,7 @@ export function useInternalNotification(
|
||||
style,
|
||||
role = 'alert',
|
||||
closeIcon,
|
||||
closable,
|
||||
...restConfig
|
||||
} = config;
|
||||
|
||||
@ -178,7 +179,7 @@ export function useInternalNotification(
|
||||
),
|
||||
style: { ...notification?.style, ...style },
|
||||
closeIcon: realCloseIcon,
|
||||
closable: !!realCloseIcon,
|
||||
closable: closable ?? !!realCloseIcon,
|
||||
});
|
||||
};
|
||||
|
||||
|
@ -2,11 +2,9 @@ import * as React from 'react';
|
||||
import ExclamationCircleFilled from '@ant-design/icons/ExclamationCircleFilled';
|
||||
import classNames from 'classnames';
|
||||
import useMergedState from 'rc-util/lib/hooks/useMergedState';
|
||||
import KeyCode from 'rc-util/lib/KeyCode';
|
||||
import omit from 'rc-util/lib/omit';
|
||||
|
||||
import type { RenderFunction } from '../_util/getRenderPropValue';
|
||||
import { cloneElement } from '../_util/reactNode';
|
||||
import type { ButtonProps, LegacyButtonType } from '../button/button';
|
||||
import { ConfigContext } from '../config-provider';
|
||||
import Popover from '../popover';
|
||||
@ -78,18 +76,15 @@ const Popconfirm = React.forwardRef<TooltipRef, PopconfirmProps>((props, ref) =>
|
||||
props.onCancel?.call(this, e);
|
||||
};
|
||||
|
||||
const onKeyDown = (e: React.KeyboardEvent<HTMLDivElement>) => {
|
||||
if (e.keyCode === KeyCode.ESC && open) {
|
||||
settingOpen(false, e);
|
||||
}
|
||||
};
|
||||
|
||||
const onInternalOpenChange = (value: boolean) => {
|
||||
const onInternalOpenChange = (
|
||||
value: boolean,
|
||||
e?: React.MouseEvent<HTMLButtonElement> | React.KeyboardEvent<HTMLDivElement>,
|
||||
) => {
|
||||
const { disabled = false } = props;
|
||||
if (disabled) {
|
||||
return;
|
||||
}
|
||||
settingOpen(value);
|
||||
settingOpen(value, e);
|
||||
};
|
||||
|
||||
const prefixCls = getPrefixCls('popconfirm', customizePrefixCls);
|
||||
@ -119,14 +114,7 @@ const Popconfirm = React.forwardRef<TooltipRef, PopconfirmProps>((props, ref) =>
|
||||
}
|
||||
data-popover-inject
|
||||
>
|
||||
{cloneElement(children, {
|
||||
onKeyDown: (e: React.KeyboardEvent<any>) => {
|
||||
if (React.isValidElement(children)) {
|
||||
children?.props.onKeyDown?.(e);
|
||||
}
|
||||
onKeyDown(e);
|
||||
},
|
||||
})}
|
||||
{children}
|
||||
</Popover>,
|
||||
);
|
||||
}) as React.ForwardRefExoticComponent<
|
||||
|
@ -11,6 +11,11 @@ const { _InternalPanelDoNotUseOrYouWillBeFired: InternalPanelDoNotUseOrYouWillBe
|
||||
describe('Popover', () => {
|
||||
mountTest(Popover);
|
||||
|
||||
const eventObject = expect.objectContaining({
|
||||
target: expect.anything(),
|
||||
preventDefault: expect.any(Function),
|
||||
});
|
||||
|
||||
it('should show overlay when trigger is clicked', () => {
|
||||
const ref = React.createRef<TooltipRef>();
|
||||
const { container } = render(
|
||||
@ -94,4 +99,20 @@ describe('Popover', () => {
|
||||
render(<InternalPanelDoNotUseOrYouWillBeFired content={null} title={null} trigger="click" />);
|
||||
}).not.toThrow();
|
||||
});
|
||||
|
||||
it('should be closed by pressing ESC', () => {
|
||||
const onOpenChange = jest.fn((_, e) => {
|
||||
e?.persist?.();
|
||||
});
|
||||
const wrapper = render(
|
||||
<Popover title="Title" trigger="click" onOpenChange={onOpenChange}>
|
||||
<span>Delete</span>
|
||||
</Popover>,
|
||||
);
|
||||
const triggerNode = wrapper.container.querySelectorAll('span')[0];
|
||||
fireEvent.click(triggerNode);
|
||||
expect(onOpenChange).toHaveBeenLastCalledWith(true, undefined);
|
||||
fireEvent.keyDown(triggerNode, { key: 'Escape', keyCode: 27 });
|
||||
expect(onOpenChange).toHaveBeenLastCalledWith(false, eventObject);
|
||||
});
|
||||
});
|
||||
|
@ -11,9 +11,17 @@ import PurePanel from './PurePanel';
|
||||
// CSSINJS
|
||||
import useStyle from './style';
|
||||
|
||||
import KeyCode from 'rc-util/lib/KeyCode';
|
||||
import { cloneElement } from '../_util/reactNode';
|
||||
import useMergedState from 'rc-util/lib/hooks/useMergedState';
|
||||
|
||||
export interface PopoverProps extends AbstractTooltipProps {
|
||||
title?: React.ReactNode | RenderFunction;
|
||||
content?: React.ReactNode | RenderFunction;
|
||||
onOpenChange?: (
|
||||
open: boolean,
|
||||
e?: React.MouseEvent<HTMLElement> | React.KeyboardEvent<HTMLDivElement>,
|
||||
) => void;
|
||||
}
|
||||
|
||||
interface OverlayProps {
|
||||
@ -37,8 +45,10 @@ const Popover = React.forwardRef<TooltipRef, PopoverProps>((props, ref) => {
|
||||
overlayClassName,
|
||||
placement = 'top',
|
||||
trigger = 'hover',
|
||||
children,
|
||||
mouseEnterDelay = 0.1,
|
||||
mouseLeaveDelay = 0.1,
|
||||
onOpenChange,
|
||||
overlayStyle = {},
|
||||
...otherProps
|
||||
} = props;
|
||||
@ -49,6 +59,27 @@ const Popover = React.forwardRef<TooltipRef, PopoverProps>((props, ref) => {
|
||||
const rootPrefixCls = getPrefixCls();
|
||||
|
||||
const overlayCls = classNames(overlayClassName, hashId, cssVarCls);
|
||||
const [open, setOpen] = useMergedState(false, {
|
||||
value: props.open ?? props.visible,
|
||||
});
|
||||
|
||||
const settingOpen = (
|
||||
value: boolean,
|
||||
e?: React.MouseEvent<HTMLButtonElement> | React.KeyboardEvent<HTMLDivElement>,
|
||||
) => {
|
||||
setOpen(value, true);
|
||||
onOpenChange?.(value, e);
|
||||
};
|
||||
|
||||
const onKeyDown = (e: React.KeyboardEvent<HTMLDivElement>) => {
|
||||
if (e.keyCode === KeyCode.ESC) {
|
||||
settingOpen(false, e);
|
||||
}
|
||||
};
|
||||
|
||||
const onInternalOpenChange = (value: boolean) => {
|
||||
settingOpen(value);
|
||||
};
|
||||
|
||||
return wrapCSSVar(
|
||||
<Tooltip
|
||||
@ -61,12 +92,23 @@ const Popover = React.forwardRef<TooltipRef, PopoverProps>((props, ref) => {
|
||||
prefixCls={prefixCls}
|
||||
overlayClassName={overlayCls}
|
||||
ref={ref}
|
||||
open={open}
|
||||
onOpenChange={onInternalOpenChange}
|
||||
overlay={
|
||||
title || content ? <Overlay prefixCls={prefixCls} title={title} content={content} /> : null
|
||||
}
|
||||
transitionName={getTransitionName(rootPrefixCls, 'zoom-big', otherProps.transitionName)}
|
||||
data-popover-inject
|
||||
/>,
|
||||
>
|
||||
{cloneElement(children, {
|
||||
onKeyDown: (e: React.KeyboardEvent<any>) => {
|
||||
if (React.isValidElement(children)) {
|
||||
children?.props.onKeyDown?.(e);
|
||||
}
|
||||
onKeyDown(e);
|
||||
},
|
||||
})}
|
||||
</Tooltip>,
|
||||
);
|
||||
}) as React.ForwardRefExoticComponent<
|
||||
React.PropsWithoutRef<PopoverProps> & React.RefAttributes<unknown>
|
||||
|
@ -29,6 +29,7 @@ const Circle: React.FC<CircleProps> = (props) => {
|
||||
children,
|
||||
success,
|
||||
size = originWidth,
|
||||
steps,
|
||||
} = props;
|
||||
|
||||
const [width, height] = getSize(size, 'circle');
|
||||
@ -51,6 +52,7 @@ const Circle: React.FC<CircleProps> = (props) => {
|
||||
return undefined;
|
||||
}, [gapDegree, type]);
|
||||
|
||||
const percentArray = getPercentage(props);
|
||||
const gapPos = gapPosition || (type === 'dashboard' && 'bottom') || undefined;
|
||||
|
||||
// using className to style stroke color
|
||||
@ -63,10 +65,11 @@ const Circle: React.FC<CircleProps> = (props) => {
|
||||
|
||||
const circleContent = (
|
||||
<RCCircle
|
||||
percent={getPercentage(props)}
|
||||
steps={steps}
|
||||
percent={steps ? percentArray[1] : percentArray}
|
||||
strokeWidth={strokeWidth}
|
||||
trailWidth={strokeWidth}
|
||||
strokeColor={strokeColor}
|
||||
strokeColor={steps ? strokeColor[1] : strokeColor}
|
||||
strokeLinecap={strokeLinecap}
|
||||
trailColor={trailColor}
|
||||
prefixCls={prefixCls}
|
||||
|
@ -485,6 +485,296 @@ exports[`renders components/progress/demo/circle-mini.tsx extend context correct
|
||||
|
||||
exports[`renders components/progress/demo/circle-mini.tsx extend context correctly 2`] = `[]`;
|
||||
|
||||
exports[`renders components/progress/demo/circle-steps.tsx extend context correctly 1`] = `
|
||||
Array [
|
||||
<h5
|
||||
class="ant-typography"
|
||||
>
|
||||
Custom count:
|
||||
</h5>,
|
||||
<div
|
||||
class="ant-slider ant-slider-horizontal"
|
||||
>
|
||||
<div
|
||||
class="ant-slider-rail"
|
||||
/>
|
||||
<div
|
||||
class="ant-slider-track"
|
||||
style="left: 0%; width: 37.5%;"
|
||||
/>
|
||||
<div
|
||||
class="ant-slider-step"
|
||||
/>
|
||||
<div
|
||||
aria-disabled="false"
|
||||
aria-orientation="horizontal"
|
||||
aria-valuemax="10"
|
||||
aria-valuemin="2"
|
||||
aria-valuenow="5"
|
||||
class="ant-slider-handle"
|
||||
role="slider"
|
||||
style="left: 37.5%; transform: translateX(-50%);"
|
||||
tabindex="0"
|
||||
/>
|
||||
<div
|
||||
class="ant-tooltip ant-zoom-big-fast-appear ant-zoom-big-fast-appear-prepare ant-zoom-big-fast ant-slider-tooltip 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"
|
||||
role="tooltip"
|
||||
>
|
||||
5
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>,
|
||||
<h5
|
||||
class="ant-typography"
|
||||
>
|
||||
Custom gap:
|
||||
</h5>,
|
||||
<div
|
||||
class="ant-slider ant-slider-horizontal"
|
||||
>
|
||||
<div
|
||||
class="ant-slider-rail"
|
||||
/>
|
||||
<div
|
||||
class="ant-slider-track"
|
||||
style="left: 0%; width: 20%;"
|
||||
/>
|
||||
<div
|
||||
class="ant-slider-step"
|
||||
/>
|
||||
<div
|
||||
aria-disabled="false"
|
||||
aria-orientation="horizontal"
|
||||
aria-valuemax="40"
|
||||
aria-valuemin="0"
|
||||
aria-valuenow="8"
|
||||
class="ant-slider-handle"
|
||||
role="slider"
|
||||
style="left: 20%; transform: translateX(-50%);"
|
||||
tabindex="0"
|
||||
/>
|
||||
<div
|
||||
class="ant-tooltip ant-zoom-big-fast-appear ant-zoom-big-fast-appear-prepare ant-zoom-big-fast ant-slider-tooltip 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"
|
||||
role="tooltip"
|
||||
>
|
||||
8
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>,
|
||||
<div
|
||||
class="ant-flex ant-flex-wrap-wrap ant-flex-gap-middle"
|
||||
style="margin-top: 16px;"
|
||||
>
|
||||
<div
|
||||
aria-valuenow="50"
|
||||
class="ant-progress ant-progress-status-normal ant-progress-circle ant-progress-steps ant-progress-show-info ant-progress-default"
|
||||
role="progressbar"
|
||||
>
|
||||
<div
|
||||
class="ant-progress-inner"
|
||||
style="width: 120px; height: 120px; font-size: 24px;"
|
||||
>
|
||||
<svg
|
||||
class="ant-progress-circle"
|
||||
role="presentation"
|
||||
viewBox="0 0 100 100"
|
||||
>
|
||||
<circle
|
||||
class="ant-progress-circle-path"
|
||||
cx="50"
|
||||
cy="50"
|
||||
opacity="1"
|
||||
r="40"
|
||||
stroke-width="20"
|
||||
style="stroke-dasharray: 198.96753472735355px 251.32741228718345; stroke-dashoffset: 176.09659288643437; transform: rotate(127.5deg); transform-origin: 50px 50px; transition: stroke-dashoffset .3s ease 0s, stroke-dasharray .3s ease 0s, stroke .3s, stroke-width .06s ease .3s, opacity .3s ease 0s; fill-opacity: 0; transition-duration: 0s, 0s;"
|
||||
/>
|
||||
<circle
|
||||
class="ant-progress-circle-path"
|
||||
cx="50"
|
||||
cy="50"
|
||||
opacity="1"
|
||||
r="40"
|
||||
stroke-width="20"
|
||||
style="stroke-dasharray: 198.96753472735355px 251.32741228718345; stroke-dashoffset: 176.09659288643437; transform: rotate(163.125deg); transform-origin: 50px 50px; transition: stroke-dashoffset .3s ease 0s, stroke-dasharray .3s ease 0s, stroke .3s, stroke-width .06s ease .3s, opacity .3s ease 0s; fill-opacity: 0; transition-duration: 0s, 0s;"
|
||||
/>
|
||||
<circle
|
||||
class="ant-progress-circle-path"
|
||||
cx="50"
|
||||
cy="50"
|
||||
opacity="1"
|
||||
r="40"
|
||||
stroke-width="20"
|
||||
style="stroke-dasharray: 198.96753472735355px 251.32741228718345; stroke-dashoffset: 176.09659288643437; transform: rotate(198.74999999999997deg); transform-origin: 50px 50px; transition: stroke-dashoffset .3s ease 0s, stroke-dasharray .3s ease 0s, stroke .3s, stroke-width .06s ease .3s, opacity .3s ease 0s; fill-opacity: 0; transition-duration: 0s, 0s;"
|
||||
/>
|
||||
<circle
|
||||
class="ant-progress-circle-path"
|
||||
cx="50"
|
||||
cy="50"
|
||||
opacity="1"
|
||||
r="40"
|
||||
stroke-width="20"
|
||||
style="stroke-dasharray: 198.96753472735355px 251.32741228718345; stroke-dashoffset: 176.09659288643437; transform: rotate(234.37499999999994deg); transform-origin: 50px 50px; transition: stroke-dashoffset .3s ease 0s, stroke-dasharray .3s ease 0s, stroke .3s, stroke-width .06s ease .3s, opacity .3s ease 0s; fill-opacity: 0; transition-duration: 0s, 0s;"
|
||||
/>
|
||||
<circle
|
||||
class="ant-progress-circle-path"
|
||||
cx="50"
|
||||
cy="50"
|
||||
opacity="1"
|
||||
r="40"
|
||||
stroke-width="20"
|
||||
style="stroke: rgba(0, 0, 0, 0.06); stroke-dasharray: 198.96753472735355px 251.32741228718345; stroke-dashoffset: 176.09659288643437; transform: rotate(269.99999999999994deg); transform-origin: 50px 50px; transition: stroke-dashoffset .3s ease 0s, stroke-dasharray .3s ease 0s, stroke .3s, stroke-width .06s ease .3s, opacity .3s ease 0s; fill-opacity: 0; transition-duration: 0s, 0s;"
|
||||
/>
|
||||
<circle
|
||||
class="ant-progress-circle-path"
|
||||
cx="50"
|
||||
cy="50"
|
||||
opacity="1"
|
||||
r="40"
|
||||
stroke-width="20"
|
||||
style="stroke: rgba(0, 0, 0, 0.06); stroke-dasharray: 198.96753472735355px 251.32741228718345; stroke-dashoffset: 176.09659288643437; transform: rotate(305.625deg); transform-origin: 50px 50px; transition: stroke-dashoffset .3s ease 0s, stroke-dasharray .3s ease 0s, stroke .3s, stroke-width .06s ease .3s, opacity .3s ease 0s; fill-opacity: 0; transition-duration: 0s, 0s;"
|
||||
/>
|
||||
<circle
|
||||
class="ant-progress-circle-path"
|
||||
cx="50"
|
||||
cy="50"
|
||||
opacity="1"
|
||||
r="40"
|
||||
stroke-width="20"
|
||||
style="stroke: rgba(0, 0, 0, 0.06); stroke-dasharray: 198.96753472735355px 251.32741228718345; stroke-dashoffset: 176.09659288643437; transform: rotate(341.24999999999994deg); transform-origin: 50px 50px; transition: stroke-dashoffset .3s ease 0s, stroke-dasharray .3s ease 0s, stroke .3s, stroke-width .06s ease .3s, opacity .3s ease 0s; fill-opacity: 0; transition-duration: 0s, 0s;"
|
||||
/>
|
||||
<circle
|
||||
class="ant-progress-circle-path"
|
||||
cx="50"
|
||||
cy="50"
|
||||
opacity="1"
|
||||
r="40"
|
||||
stroke-width="20"
|
||||
style="stroke: rgba(0, 0, 0, 0.06); stroke-dasharray: 198.96753472735355px 251.32741228718345; stroke-dashoffset: 176.09659288643437; transform: rotate(376.87499999999994deg); transform-origin: 50px 50px; transition: stroke-dashoffset .3s ease 0s, stroke-dasharray .3s ease 0s, stroke .3s, stroke-width .06s ease .3s, opacity .3s ease 0s; fill-opacity: 0; transition-duration: 0s, 0s;"
|
||||
/>
|
||||
</svg>
|
||||
<span
|
||||
class="ant-progress-text"
|
||||
title="50%"
|
||||
>
|
||||
50%
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
aria-valuenow="100"
|
||||
class="ant-progress ant-progress-status-success ant-progress-circle ant-progress-steps ant-progress-show-info ant-progress-default"
|
||||
role="progressbar"
|
||||
>
|
||||
<div
|
||||
class="ant-progress-inner"
|
||||
style="width: 120px; height: 120px; font-size: 24px;"
|
||||
>
|
||||
<svg
|
||||
class="ant-progress-circle"
|
||||
role="presentation"
|
||||
viewBox="0 0 100 100"
|
||||
>
|
||||
<circle
|
||||
class="ant-progress-circle-path"
|
||||
cx="50"
|
||||
cy="50"
|
||||
opacity="1"
|
||||
r="40"
|
||||
stroke-width="20"
|
||||
style="stroke-dasharray: 251.32741228718345px 251.32741228718345; stroke-dashoffset: 208.06192982974676; transform: rotate(-90deg); transform-origin: 50px 50px; transition: stroke-dashoffset .3s ease 0s, stroke-dasharray .3s ease 0s, stroke .3s, stroke-width .06s ease .3s, opacity .3s ease 0s; fill-opacity: 0; transition-duration: 0s, 0s;"
|
||||
/>
|
||||
<circle
|
||||
class="ant-progress-circle-path"
|
||||
cx="50"
|
||||
cy="50"
|
||||
opacity="1"
|
||||
r="40"
|
||||
stroke-width="20"
|
||||
style="stroke-dasharray: 251.32741228718345px 251.32741228718345; stroke-dashoffset: 208.06192982974676; transform: rotate(-18deg); transform-origin: 50px 50px; transition: stroke-dashoffset .3s ease 0s, stroke-dasharray .3s ease 0s, stroke .3s, stroke-width .06s ease .3s, opacity .3s ease 0s; fill-opacity: 0; transition-duration: 0s, 0s;"
|
||||
/>
|
||||
<circle
|
||||
class="ant-progress-circle-path"
|
||||
cx="50"
|
||||
cy="50"
|
||||
opacity="1"
|
||||
r="40"
|
||||
stroke-width="20"
|
||||
style="stroke-dasharray: 251.32741228718345px 251.32741228718345; stroke-dashoffset: 208.06192982974676; transform: rotate(54deg); transform-origin: 50px 50px; transition: stroke-dashoffset .3s ease 0s, stroke-dasharray .3s ease 0s, stroke .3s, stroke-width .06s ease .3s, opacity .3s ease 0s; fill-opacity: 0; transition-duration: 0s, 0s;"
|
||||
/>
|
||||
<circle
|
||||
class="ant-progress-circle-path"
|
||||
cx="50"
|
||||
cy="50"
|
||||
opacity="1"
|
||||
r="40"
|
||||
stroke-width="20"
|
||||
style="stroke-dasharray: 251.32741228718345px 251.32741228718345; stroke-dashoffset: 208.06192982974676; transform: rotate(126deg); transform-origin: 50px 50px; transition: stroke-dashoffset .3s ease 0s, stroke-dasharray .3s ease 0s, stroke .3s, stroke-width .06s ease .3s, opacity .3s ease 0s; fill-opacity: 0; transition-duration: 0s, 0s;"
|
||||
/>
|
||||
<circle
|
||||
class="ant-progress-circle-path"
|
||||
cx="50"
|
||||
cy="50"
|
||||
opacity="1"
|
||||
r="40"
|
||||
stroke-width="20"
|
||||
style="stroke-dasharray: 251.32741228718345px 251.32741228718345; stroke-dashoffset: 208.06192982974676; transform: rotate(198deg); transform-origin: 50px 50px; transition: stroke-dashoffset .3s ease 0s, stroke-dasharray .3s ease 0s, stroke .3s, stroke-width .06s ease .3s, opacity .3s ease 0s; fill-opacity: 0; transition-duration: 0s, 0s;"
|
||||
/>
|
||||
</svg>
|
||||
<span
|
||||
class="ant-progress-text"
|
||||
>
|
||||
<span
|
||||
aria-label="check"
|
||||
class="anticon anticon-check"
|
||||
role="img"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
data-icon="check"
|
||||
fill="currentColor"
|
||||
focusable="false"
|
||||
height="1em"
|
||||
viewBox="64 64 896 896"
|
||||
width="1em"
|
||||
>
|
||||
<path
|
||||
d="M912 190h-69.9c-9.8 0-19.1 4.5-25.1 12.2L404.7 724.5 207 474a32 32 0 00-25.1-12.2H112c-6.7 0-10.4 7.7-6.3 12.9l273.9 347c12.8 16.2 37.4 16.2 50.3 0l488.4-618.9c4.1-5.1.4-12.8-6.3-12.8z"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>,
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`renders components/progress/demo/circle-steps.tsx extend context correctly 2`] = `[]`;
|
||||
|
||||
exports[`renders components/progress/demo/component-token.tsx extend context correctly 1`] = `
|
||||
Array [
|
||||
<div
|
||||
|
@ -455,6 +455,256 @@ exports[`renders components/progress/demo/circle-mini.tsx correctly 1`] = `
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`renders components/progress/demo/circle-steps.tsx correctly 1`] = `
|
||||
Array [
|
||||
<h5
|
||||
class="ant-typography"
|
||||
>
|
||||
Custom count:
|
||||
</h5>,
|
||||
<div
|
||||
class="ant-slider ant-slider-horizontal"
|
||||
>
|
||||
<div
|
||||
class="ant-slider-rail"
|
||||
/>
|
||||
<div
|
||||
class="ant-slider-track"
|
||||
style="left:0%;width:37.5%"
|
||||
/>
|
||||
<div
|
||||
class="ant-slider-step"
|
||||
/>
|
||||
<div
|
||||
aria-disabled="false"
|
||||
aria-orientation="horizontal"
|
||||
aria-valuemax="10"
|
||||
aria-valuemin="2"
|
||||
aria-valuenow="5"
|
||||
class="ant-slider-handle"
|
||||
role="slider"
|
||||
style="left:37.5%;transform:translateX(-50%)"
|
||||
tabindex="0"
|
||||
/>
|
||||
</div>,
|
||||
<h5
|
||||
class="ant-typography"
|
||||
>
|
||||
Custom gap:
|
||||
</h5>,
|
||||
<div
|
||||
class="ant-slider ant-slider-horizontal"
|
||||
>
|
||||
<div
|
||||
class="ant-slider-rail"
|
||||
/>
|
||||
<div
|
||||
class="ant-slider-track"
|
||||
style="left:0%;width:20%"
|
||||
/>
|
||||
<div
|
||||
class="ant-slider-step"
|
||||
/>
|
||||
<div
|
||||
aria-disabled="false"
|
||||
aria-orientation="horizontal"
|
||||
aria-valuemax="40"
|
||||
aria-valuemin="0"
|
||||
aria-valuenow="8"
|
||||
class="ant-slider-handle"
|
||||
role="slider"
|
||||
style="left:20%;transform:translateX(-50%)"
|
||||
tabindex="0"
|
||||
/>
|
||||
</div>,
|
||||
<div
|
||||
class="ant-flex ant-flex-wrap-wrap ant-flex-gap-middle"
|
||||
style="margin-top:16px"
|
||||
>
|
||||
<div
|
||||
aria-valuenow="50"
|
||||
class="ant-progress ant-progress-status-normal ant-progress-circle ant-progress-steps ant-progress-show-info ant-progress-default"
|
||||
role="progressbar"
|
||||
>
|
||||
<div
|
||||
class="ant-progress-inner"
|
||||
style="width:120px;height:120px;font-size:24px"
|
||||
>
|
||||
<svg
|
||||
class="ant-progress-circle"
|
||||
role="presentation"
|
||||
viewBox="0 0 100 100"
|
||||
>
|
||||
<circle
|
||||
class="ant-progress-circle-path"
|
||||
cx="50"
|
||||
cy="50"
|
||||
opacity="1"
|
||||
r="40"
|
||||
stroke-width="20"
|
||||
style="stroke-dasharray:198.96753472735355px 251.32741228718345;stroke-dashoffset:176.09659288643437;transform:rotate(127.5deg);transform-origin:50px 50px;transition:stroke-dashoffset .3s ease 0s, stroke-dasharray .3s ease 0s, stroke .3s, stroke-width .06s ease .3s, opacity .3s ease 0s;fill-opacity:0"
|
||||
/>
|
||||
<circle
|
||||
class="ant-progress-circle-path"
|
||||
cx="50"
|
||||
cy="50"
|
||||
opacity="1"
|
||||
r="40"
|
||||
stroke-width="20"
|
||||
style="stroke-dasharray:198.96753472735355px 251.32741228718345;stroke-dashoffset:176.09659288643437;transform:rotate(163.125deg);transform-origin:50px 50px;transition:stroke-dashoffset .3s ease 0s, stroke-dasharray .3s ease 0s, stroke .3s, stroke-width .06s ease .3s, opacity .3s ease 0s;fill-opacity:0"
|
||||
/>
|
||||
<circle
|
||||
class="ant-progress-circle-path"
|
||||
cx="50"
|
||||
cy="50"
|
||||
opacity="1"
|
||||
r="40"
|
||||
stroke-width="20"
|
||||
style="stroke-dasharray:198.96753472735355px 251.32741228718345;stroke-dashoffset:176.09659288643437;transform:rotate(198.74999999999997deg);transform-origin:50px 50px;transition:stroke-dashoffset .3s ease 0s, stroke-dasharray .3s ease 0s, stroke .3s, stroke-width .06s ease .3s, opacity .3s ease 0s;fill-opacity:0"
|
||||
/>
|
||||
<circle
|
||||
class="ant-progress-circle-path"
|
||||
cx="50"
|
||||
cy="50"
|
||||
opacity="1"
|
||||
r="40"
|
||||
stroke-width="20"
|
||||
style="stroke-dasharray:198.96753472735355px 251.32741228718345;stroke-dashoffset:176.09659288643437;transform:rotate(234.37499999999994deg);transform-origin:50px 50px;transition:stroke-dashoffset .3s ease 0s, stroke-dasharray .3s ease 0s, stroke .3s, stroke-width .06s ease .3s, opacity .3s ease 0s;fill-opacity:0"
|
||||
/>
|
||||
<circle
|
||||
class="ant-progress-circle-path"
|
||||
cx="50"
|
||||
cy="50"
|
||||
opacity="1"
|
||||
r="40"
|
||||
stroke-width="20"
|
||||
style="stroke:rgba(0, 0, 0, 0.06);stroke-dasharray:198.96753472735355px 251.32741228718345;stroke-dashoffset:176.09659288643437;transform:rotate(269.99999999999994deg);transform-origin:50px 50px;transition:stroke-dashoffset .3s ease 0s, stroke-dasharray .3s ease 0s, stroke .3s, stroke-width .06s ease .3s, opacity .3s ease 0s;fill-opacity:0"
|
||||
/>
|
||||
<circle
|
||||
class="ant-progress-circle-path"
|
||||
cx="50"
|
||||
cy="50"
|
||||
opacity="1"
|
||||
r="40"
|
||||
stroke-width="20"
|
||||
style="stroke:rgba(0, 0, 0, 0.06);stroke-dasharray:198.96753472735355px 251.32741228718345;stroke-dashoffset:176.09659288643437;transform:rotate(305.625deg);transform-origin:50px 50px;transition:stroke-dashoffset .3s ease 0s, stroke-dasharray .3s ease 0s, stroke .3s, stroke-width .06s ease .3s, opacity .3s ease 0s;fill-opacity:0"
|
||||
/>
|
||||
<circle
|
||||
class="ant-progress-circle-path"
|
||||
cx="50"
|
||||
cy="50"
|
||||
opacity="1"
|
||||
r="40"
|
||||
stroke-width="20"
|
||||
style="stroke:rgba(0, 0, 0, 0.06);stroke-dasharray:198.96753472735355px 251.32741228718345;stroke-dashoffset:176.09659288643437;transform:rotate(341.24999999999994deg);transform-origin:50px 50px;transition:stroke-dashoffset .3s ease 0s, stroke-dasharray .3s ease 0s, stroke .3s, stroke-width .06s ease .3s, opacity .3s ease 0s;fill-opacity:0"
|
||||
/>
|
||||
<circle
|
||||
class="ant-progress-circle-path"
|
||||
cx="50"
|
||||
cy="50"
|
||||
opacity="1"
|
||||
r="40"
|
||||
stroke-width="20"
|
||||
style="stroke:rgba(0, 0, 0, 0.06);stroke-dasharray:198.96753472735355px 251.32741228718345;stroke-dashoffset:176.09659288643437;transform:rotate(376.87499999999994deg);transform-origin:50px 50px;transition:stroke-dashoffset .3s ease 0s, stroke-dasharray .3s ease 0s, stroke .3s, stroke-width .06s ease .3s, opacity .3s ease 0s;fill-opacity:0"
|
||||
/>
|
||||
</svg>
|
||||
<span
|
||||
class="ant-progress-text"
|
||||
title="50%"
|
||||
>
|
||||
50%
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
aria-valuenow="100"
|
||||
class="ant-progress ant-progress-status-success ant-progress-circle ant-progress-steps ant-progress-show-info ant-progress-default"
|
||||
role="progressbar"
|
||||
>
|
||||
<div
|
||||
class="ant-progress-inner"
|
||||
style="width:120px;height:120px;font-size:24px"
|
||||
>
|
||||
<svg
|
||||
class="ant-progress-circle"
|
||||
role="presentation"
|
||||
viewBox="0 0 100 100"
|
||||
>
|
||||
<circle
|
||||
class="ant-progress-circle-path"
|
||||
cx="50"
|
||||
cy="50"
|
||||
opacity="1"
|
||||
r="40"
|
||||
stroke-width="20"
|
||||
style="stroke-dasharray:251.32741228718345px 251.32741228718345;stroke-dashoffset:208.06192982974676;transform:rotate(-90deg);transform-origin:50px 50px;transition:stroke-dashoffset .3s ease 0s, stroke-dasharray .3s ease 0s, stroke .3s, stroke-width .06s ease .3s, opacity .3s ease 0s;fill-opacity:0"
|
||||
/>
|
||||
<circle
|
||||
class="ant-progress-circle-path"
|
||||
cx="50"
|
||||
cy="50"
|
||||
opacity="1"
|
||||
r="40"
|
||||
stroke-width="20"
|
||||
style="stroke-dasharray:251.32741228718345px 251.32741228718345;stroke-dashoffset:208.06192982974676;transform:rotate(-18deg);transform-origin:50px 50px;transition:stroke-dashoffset .3s ease 0s, stroke-dasharray .3s ease 0s, stroke .3s, stroke-width .06s ease .3s, opacity .3s ease 0s;fill-opacity:0"
|
||||
/>
|
||||
<circle
|
||||
class="ant-progress-circle-path"
|
||||
cx="50"
|
||||
cy="50"
|
||||
opacity="1"
|
||||
r="40"
|
||||
stroke-width="20"
|
||||
style="stroke-dasharray:251.32741228718345px 251.32741228718345;stroke-dashoffset:208.06192982974676;transform:rotate(54deg);transform-origin:50px 50px;transition:stroke-dashoffset .3s ease 0s, stroke-dasharray .3s ease 0s, stroke .3s, stroke-width .06s ease .3s, opacity .3s ease 0s;fill-opacity:0"
|
||||
/>
|
||||
<circle
|
||||
class="ant-progress-circle-path"
|
||||
cx="50"
|
||||
cy="50"
|
||||
opacity="1"
|
||||
r="40"
|
||||
stroke-width="20"
|
||||
style="stroke-dasharray:251.32741228718345px 251.32741228718345;stroke-dashoffset:208.06192982974676;transform:rotate(126deg);transform-origin:50px 50px;transition:stroke-dashoffset .3s ease 0s, stroke-dasharray .3s ease 0s, stroke .3s, stroke-width .06s ease .3s, opacity .3s ease 0s;fill-opacity:0"
|
||||
/>
|
||||
<circle
|
||||
class="ant-progress-circle-path"
|
||||
cx="50"
|
||||
cy="50"
|
||||
opacity="1"
|
||||
r="40"
|
||||
stroke-width="20"
|
||||
style="stroke-dasharray:251.32741228718345px 251.32741228718345;stroke-dashoffset:208.06192982974676;transform:rotate(198deg);transform-origin:50px 50px;transition:stroke-dashoffset .3s ease 0s, stroke-dasharray .3s ease 0s, stroke .3s, stroke-width .06s ease .3s, opacity .3s ease 0s;fill-opacity:0"
|
||||
/>
|
||||
</svg>
|
||||
<span
|
||||
class="ant-progress-text"
|
||||
>
|
||||
<span
|
||||
aria-label="check"
|
||||
class="anticon anticon-check"
|
||||
role="img"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
data-icon="check"
|
||||
fill="currentColor"
|
||||
focusable="false"
|
||||
height="1em"
|
||||
viewBox="64 64 896 896"
|
||||
width="1em"
|
||||
>
|
||||
<path
|
||||
d="M912 190h-69.9c-9.8 0-19.1 4.5-25.1 12.2L404.7 724.5 207 474a32 32 0 00-25.1-12.2H112c-6.7 0-10.4 7.7-6.3 12.9l273.9 347c12.8 16.2 37.4 16.2 50.3 0l488.4-618.9c4.1-5.1.4-12.8-6.3-12.8z"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>,
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`renders components/progress/demo/component-token.tsx correctly 1`] = `
|
||||
Array [
|
||||
<div
|
||||
|
@ -1,5 +1,83 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`Progress circle progress should accept steps 1`] = `
|
||||
<div
|
||||
aria-valuenow="70"
|
||||
class="ant-progress ant-progress-status-normal ant-progress-steps ant-progress-show-info ant-progress-default"
|
||||
role="progressbar"
|
||||
>
|
||||
<div
|
||||
class="ant-progress-steps-outer"
|
||||
>
|
||||
<div
|
||||
class="ant-progress-steps-item ant-progress-steps-item-active"
|
||||
style="width: 14px; height: 8px;"
|
||||
/>
|
||||
<div
|
||||
class="ant-progress-steps-item ant-progress-steps-item-active"
|
||||
style="width: 14px; height: 8px;"
|
||||
/>
|
||||
<div
|
||||
class="ant-progress-steps-item ant-progress-steps-item-active"
|
||||
style="width: 14px; height: 8px;"
|
||||
/>
|
||||
<div
|
||||
class="ant-progress-steps-item ant-progress-steps-item-active"
|
||||
style="width: 14px; height: 8px;"
|
||||
/>
|
||||
<div
|
||||
class="ant-progress-steps-item"
|
||||
style="width: 14px; height: 8px;"
|
||||
/>
|
||||
<span
|
||||
class="ant-progress-text"
|
||||
title="70%"
|
||||
>
|
||||
70%
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`Progress circle progress steps can be number 1`] = `
|
||||
<div
|
||||
aria-valuenow="70"
|
||||
class="ant-progress ant-progress-status-normal ant-progress-steps ant-progress-show-info ant-progress-default"
|
||||
role="progressbar"
|
||||
>
|
||||
<div
|
||||
class="ant-progress-steps-outer"
|
||||
>
|
||||
<div
|
||||
class="ant-progress-steps-item ant-progress-steps-item-active"
|
||||
style="width: 14px; height: 8px;"
|
||||
/>
|
||||
<div
|
||||
class="ant-progress-steps-item ant-progress-steps-item-active"
|
||||
style="width: 14px; height: 8px;"
|
||||
/>
|
||||
<div
|
||||
class="ant-progress-steps-item ant-progress-steps-item-active"
|
||||
style="width: 14px; height: 8px;"
|
||||
/>
|
||||
<div
|
||||
class="ant-progress-steps-item ant-progress-steps-item-active"
|
||||
style="width: 14px; height: 8px;"
|
||||
/>
|
||||
<div
|
||||
class="ant-progress-steps-item"
|
||||
style="width: 14px; height: 8px;"
|
||||
/>
|
||||
<span
|
||||
class="ant-progress-text"
|
||||
title="70%"
|
||||
>
|
||||
70%
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`Progress render dashboard 295 gapDegree 1`] = `
|
||||
<div
|
||||
aria-valuenow="0"
|
||||
|
@ -404,4 +404,14 @@ describe('Progress', () => {
|
||||
expect(progress).toHaveAttribute('aria-labelledby', 'progressLabel');
|
||||
expect(progress).toHaveAttribute('aria-valuenow', '90');
|
||||
});
|
||||
|
||||
it('circle progress should accept steps', () => {
|
||||
const { container } = render(<Progress percent={70} steps={{ count: 5, gap: 5 }} />);
|
||||
expect(container.firstChild).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('circle progress steps can be number', () => {
|
||||
const { container } = render(<Progress percent={70} steps={5} />);
|
||||
expect(container.firstChild).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
7
components/progress/demo/circle-steps.md
Normal file
7
components/progress/demo/circle-steps.md
Normal file
@ -0,0 +1,7 @@
|
||||
## zh-CN
|
||||
|
||||
步骤进度圈,支持颜色分段展示,默认间隔为 2px。
|
||||
|
||||
## en-US
|
||||
|
||||
A circular progress bar that support steps and color segments, default gap is 2px.
|
33
components/progress/demo/circle-steps.tsx
Normal file
33
components/progress/demo/circle-steps.tsx
Normal file
@ -0,0 +1,33 @@
|
||||
import React from 'react';
|
||||
import { Flex, Progress, Slider, Typography } from 'antd';
|
||||
|
||||
const App: React.FC = () => {
|
||||
const [stepsCount, setStepsCount] = React.useState<number>(5);
|
||||
const [stepsGap, setStepsGap] = React.useState<number>(7);
|
||||
return (
|
||||
<>
|
||||
<Typography.Title level={5}>Custom count:</Typography.Title>
|
||||
<Slider min={2} max={10} value={stepsCount} onChange={setStepsCount} />
|
||||
<Typography.Title level={5}>Custom gap:</Typography.Title>
|
||||
<Slider step={4} min={0} max={40} value={stepsGap} onChange={setStepsGap} />
|
||||
<Flex wrap="wrap" gap="middle" style={{ marginTop: 16 }}>
|
||||
<Progress
|
||||
type="dashboard"
|
||||
steps={8}
|
||||
percent={50}
|
||||
trailColor="rgba(0, 0, 0, 0.06)"
|
||||
strokeWidth={20}
|
||||
/>
|
||||
<Progress
|
||||
type="circle"
|
||||
percent={100}
|
||||
steps={{ count: stepsCount, gap: stepsGap }}
|
||||
trailColor="rgba(0, 0, 0, 0.06)"
|
||||
strokeWidth={20}
|
||||
/>
|
||||
</Flex>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default App;
|
@ -31,6 +31,7 @@ If it will take a long time to complete an operation, you can use `Progress` to
|
||||
<code src="./demo/linecap.tsx">Stroke Linecap</code>
|
||||
<code src="./demo/gradient-line.tsx">Custom line gradient</code>
|
||||
<code src="./demo/steps.tsx">Progress bar with steps</code>
|
||||
<code src="./demo/circle-steps.tsx" version="5.16.0">Circular progress bar whit steps</code>
|
||||
<code src="./demo/size.tsx">Progress size</code>
|
||||
|
||||
## API
|
||||
@ -63,13 +64,15 @@ Properties that shared by all types.
|
||||
|
||||
| Property | Description | Type | Default | Version |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| steps | The total step count.When passing an object, `count` refers to the number of steps, and `gap` refers to the distance between them.When passing number, the default value for `gap` is 2. | number \| { count: number, gap: number } | - | 5.16.0 |
|
||||
| strokeColor | The color of circular progress, render gradient when passing an object | string \| { number%: string } | - | - |
|
||||
| strokeWidth | To set the width of the circular progress, unit: percentage of the canvas width | number | 6 | - |
|
||||
|
||||
### `type="dashboard"`
|
||||
|
||||
| Property | Description | Type | Default |
|
||||
| --- | --- | --- | --- |
|
||||
| Property | Description | Type | Default | Version |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| steps | The total step count.When passing an object, `count` refers to the number of steps, and `gap` refers to the distance between them.When passing number, the default value for `gap` is 2. | number \| { count: number, gap: number } | - | 5.16.0 |
|
||||
| gapDegree | The gap degree of half circle, 0 ~ 295 | number | 75 |
|
||||
| gapPosition | The gap position, options: `top` `bottom` `left` `right` | string | `bottom` |
|
||||
| strokeWidth | To set the width of the dashboard progress, unit: percentage of the canvas width | number | 6 |
|
||||
|
@ -32,6 +32,7 @@ demo:
|
||||
<code src="./demo/linecap.tsx">边缘形状</code>
|
||||
<code src="./demo/gradient-line.tsx">自定义进度条渐变色</code>
|
||||
<code src="./demo/steps.tsx">步骤进度条</code>
|
||||
<code src="./demo/circle-steps.tsx" version="5.16.0">步骤进度圈</code>
|
||||
<code src="./demo/size.tsx">尺寸</code>
|
||||
|
||||
## API
|
||||
@ -64,6 +65,7 @@ demo:
|
||||
|
||||
| 属性 | 说明 | 类型 | 默认值 | 版本 |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| steps | 进度条总共步数,传入 object 时,count 指步数,gap 指间隔大小。传 number 类型时,gap 默认为 2。 | number \| { count: number, gap: number } | - | 5.16.0 |
|
||||
| strokeColor | 圆形进度条线的色彩,传入 object 时为渐变 | string \| { number%: string } | - | - |
|
||||
| strokeWidth | 圆形进度条线的宽度,单位是进度条画布宽度的百分比 | number | 6 | - |
|
||||
|
||||
@ -71,6 +73,7 @@ demo:
|
||||
|
||||
| 属性 | 说明 | 类型 | 默认值 | 版本 |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| steps | 进度条总共步数,传入 object 时,count 指步数,gap 指间隔大小。传 number 类型时,gap 默认为 2。 | number \| { count: number, gap: number } | - | 5.16.0 |
|
||||
| gapDegree | 仪表盘进度条缺口角度,可取值 0 ~ 295 | number | 75 | - |
|
||||
| gapPosition | 仪表盘进度条缺口位置 | `top` \| `bottom` \| `left` \| `right` | `bottom` | - |
|
||||
| strokeWidth | 仪表盘进度条线的宽度,单位是进度条画布宽度的百分比 | number | 6 | - |
|
||||
|
@ -53,7 +53,7 @@ export interface ProgressProps extends ProgressAriaProps {
|
||||
gapDegree?: number;
|
||||
gapPosition?: 'top' | 'bottom' | 'left' | 'right';
|
||||
size?: number | [number | string, number] | ProgressSize;
|
||||
steps?: number;
|
||||
steps?: number | { count: number; gap: number };
|
||||
/** @deprecated Use `success` instead */
|
||||
successPercent?: number;
|
||||
children?: React.ReactNode;
|
||||
@ -148,7 +148,12 @@ const Progress = React.forwardRef<HTMLDivElement, ProgressProps>((props, ref) =>
|
||||
// Render progress shape
|
||||
if (type === 'line') {
|
||||
progress = steps ? (
|
||||
<Steps {...props} strokeColor={strokeColorNotGradient} prefixCls={prefixCls} steps={steps}>
|
||||
<Steps
|
||||
{...props}
|
||||
strokeColor={strokeColorNotGradient}
|
||||
prefixCls={prefixCls}
|
||||
steps={typeof steps === 'object' ? steps.count : steps}
|
||||
>
|
||||
{progressInfo}
|
||||
</Steps>
|
||||
) : (
|
||||
@ -177,9 +182,11 @@ const Progress = React.forwardRef<HTMLDivElement, ProgressProps>((props, ref) =>
|
||||
const classString = classNames(
|
||||
prefixCls,
|
||||
`${prefixCls}-status-${progressStatus}`,
|
||||
`${prefixCls}-${(type === 'dashboard' && 'circle') || (steps && 'steps') || type}`,
|
||||
{
|
||||
[`${prefixCls}-${(type === 'dashboard' && 'circle') || type}`]: type !== 'line',
|
||||
[`${prefixCls}-inline-circle`]: type === 'circle' && getSize(size, 'circle')[0] <= 20,
|
||||
[`${prefixCls}-line`]: !steps && type === 'line',
|
||||
[`${prefixCls}-steps`]: steps,
|
||||
[`${prefixCls}-show-info`]: showInfo,
|
||||
[`${prefixCls}-${size}`]: typeof size === 'string',
|
||||
[`${prefixCls}-rtl`]: direction === 'rtl',
|
||||
|
@ -25,7 +25,6 @@ import Pagination from '../pagination';
|
||||
import type { SpinProps } from '../spin';
|
||||
import Spin from '../spin';
|
||||
import { useToken } from '../theme/internal';
|
||||
import type { TooltipProps } from '../tooltip';
|
||||
import renderExpandIcon from './ExpandIcon';
|
||||
import useContainerWidth from './hooks/useContainerWidth';
|
||||
import type { FilterState } from './hooks/useFilter';
|
||||
@ -47,6 +46,7 @@ import type {
|
||||
GetRowKey,
|
||||
RefInternalTable,
|
||||
SorterResult,
|
||||
SorterTooltipProps,
|
||||
SortOrder,
|
||||
TableAction,
|
||||
TableCurrentDataSource,
|
||||
@ -116,7 +116,7 @@ export interface TableProps<RecordType = any>
|
||||
scrollToFirstRowOnChange?: boolean;
|
||||
};
|
||||
sortDirections?: SortOrder[];
|
||||
showSorterTooltip?: boolean | TooltipProps;
|
||||
showSorterTooltip?: boolean | SorterTooltipProps;
|
||||
virtual?: boolean;
|
||||
}
|
||||
|
||||
@ -151,7 +151,7 @@ const InternalTable = <RecordType extends AnyObject = AnyObject>(
|
||||
scroll,
|
||||
sortDirections,
|
||||
locale,
|
||||
showSorterTooltip = true,
|
||||
showSorterTooltip = { target: 'full-header' },
|
||||
virtual,
|
||||
} = props;
|
||||
|
||||
|
@ -1,5 +1,6 @@
|
||||
/* eslint-disable react/no-multi-comp */
|
||||
import React from 'react';
|
||||
|
||||
import type { ColumnType, TableProps } from '..';
|
||||
import Table from '..';
|
||||
import { act, fireEvent, render } from '../../../tests/utils';
|
||||
@ -283,7 +284,7 @@ describe('Table.sorter', () => {
|
||||
|
||||
// set table props showSorterTooltip is false, column showSorterTooltip is true
|
||||
rerender(
|
||||
createTable({ showSorterTooltip: true, columns: [{ ...column, showSorterTooltip: true }] }),
|
||||
createTable({ showSorterTooltip: false, columns: [{ ...column, showSorterTooltip: true }] }),
|
||||
);
|
||||
fireEvent.mouseEnter(container.querySelector('.ant-table-column-sorters')!);
|
||||
act(() => {
|
||||
@ -305,6 +306,96 @@ describe('Table.sorter', () => {
|
||||
});
|
||||
expect(container.querySelector('.ant-tooltip-open')).toBeFalsy();
|
||||
fireEvent.mouseOut(container.querySelector('.ant-table-column-sorters')!);
|
||||
|
||||
// table props showSorterTooltip is 'full-header' by default
|
||||
rerender(
|
||||
createTable({
|
||||
showSorterTooltip: true,
|
||||
columns: [{ ...column }],
|
||||
}),
|
||||
);
|
||||
expect(container.querySelector('.ant-table-column-sorters')).not.toHaveClass(
|
||||
'ant-table-column-sorters-tooltip-target-sorter',
|
||||
);
|
||||
fireEvent.mouseEnter(container.querySelector('.ant-table-column-sorters')!);
|
||||
act(() => {
|
||||
jest.runAllTimers();
|
||||
});
|
||||
expect(container.querySelector('.ant-tooltip-open')).toBeTruthy();
|
||||
fireEvent.mouseOut(container.querySelector('.ant-table-column-sorters')!);
|
||||
|
||||
// set table props showSorterTooltip target is 'sorter-icon'
|
||||
rerender(
|
||||
createTable({
|
||||
showSorterTooltip: { target: 'sorter-icon' },
|
||||
columns: [{ ...column }],
|
||||
}),
|
||||
);
|
||||
expect(container.querySelector('.ant-table-column-sorters')).toHaveClass(
|
||||
'ant-table-column-sorters-tooltip-target-sorter',
|
||||
);
|
||||
// hovering over the sorters element does NOT open tooltip
|
||||
fireEvent.mouseEnter(container.querySelector('.ant-table-column-sorters')!);
|
||||
act(() => {
|
||||
jest.runAllTimers();
|
||||
});
|
||||
expect(container.querySelector('.ant-tooltip-open')).toBeFalsy();
|
||||
fireEvent.mouseOut(container.querySelector('.ant-table-column-sorters')!);
|
||||
// hovering over the sorter element DOES open tooltip
|
||||
fireEvent.mouseEnter(container.querySelector('.ant-table-column-sorter')!);
|
||||
act(() => {
|
||||
jest.runAllTimers();
|
||||
});
|
||||
expect(container.querySelector('.ant-tooltip-open')).toBeTruthy();
|
||||
fireEvent.mouseOut(container.querySelector('.ant-table-column-sorter')!);
|
||||
|
||||
// set table props showSorterTooltip target is 'sorter-icon', column showSorterTooltip target is 'full-header'
|
||||
rerender(
|
||||
createTable({
|
||||
showSorterTooltip: { target: 'sorter-icon' },
|
||||
columns: [{ ...column, showSorterTooltip: { target: 'full-header' } }],
|
||||
}),
|
||||
);
|
||||
expect(container.querySelector('.ant-table-column-sorters')).not.toHaveClass(
|
||||
'ant-table-column-sorters-tooltip-target-sorter',
|
||||
);
|
||||
fireEvent.mouseEnter(container.querySelector('.ant-table-column-sorters')!);
|
||||
act(() => {
|
||||
jest.runAllTimers();
|
||||
});
|
||||
expect(container.querySelector('.ant-tooltip-open')).toBeTruthy();
|
||||
fireEvent.mouseOut(container.querySelector('.ant-table-column-sorters')!);
|
||||
|
||||
// set table props showSorterTooltip target is 'full-header', column showSorterTooltip target is 'sorter-icon'
|
||||
rerender(
|
||||
createTable({
|
||||
showSorterTooltip: { target: 'full-header' },
|
||||
columns: [{ ...column, showSorterTooltip: { target: 'sorter-icon' } }],
|
||||
}),
|
||||
);
|
||||
expect(container.querySelector('.ant-table-column-sorters')).toHaveClass(
|
||||
'ant-table-column-sorters-tooltip-target-sorter',
|
||||
);
|
||||
// hovering over the sorters element does NOT open tooltip
|
||||
fireEvent.mouseEnter(container.querySelector('.ant-table-column-sorters')!);
|
||||
act(() => {
|
||||
jest.runAllTimers();
|
||||
});
|
||||
expect(container.querySelector('.ant-tooltip-open')).toBeFalsy();
|
||||
fireEvent.mouseOut(container.querySelector('.ant-table-column-sorters')!);
|
||||
// hovering over the title element does NOT open tooltip
|
||||
fireEvent.mouseEnter(container.querySelector('.ant-table-column-title')!);
|
||||
act(() => {
|
||||
jest.runAllTimers();
|
||||
});
|
||||
expect(container.querySelector('.ant-tooltip-open')).toBeFalsy();
|
||||
// hovering over the sorter element DOES open tooltip
|
||||
fireEvent.mouseEnter(container.querySelector('.ant-table-column-sorter')!);
|
||||
act(() => {
|
||||
jest.runAllTimers();
|
||||
});
|
||||
expect(container.querySelector('.ant-tooltip-open')).toBeTruthy();
|
||||
fireEvent.mouseOut(container.querySelector('.ant-table-column-sorter')!);
|
||||
});
|
||||
|
||||
it('should show correct tooltip when showSorterTooltip is an object', () => {
|
||||
|
@ -473,4 +473,28 @@ describe('Table', () => {
|
||||
container.querySelectorAll('.ant-table-thead tr')[1].querySelectorAll('th'),
|
||||
).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('support disable row hover', () => {
|
||||
const { container } = render(
|
||||
<Table
|
||||
columns={[
|
||||
{
|
||||
title: 'Name',
|
||||
key: 'name',
|
||||
dataIndex: 'name',
|
||||
},
|
||||
]}
|
||||
dataSource={[
|
||||
{
|
||||
name: 'name1',
|
||||
},
|
||||
]}
|
||||
rowHoverable={false}
|
||||
/>,
|
||||
);
|
||||
const cell = container.querySelector('.ant-table-row .ant-table-cell')!;
|
||||
|
||||
fireEvent.mouseEnter(cell);
|
||||
expect(container.querySelectorAll('.ant-table-cell-row-hover')).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
|
@ -16446,7 +16446,7 @@ exports[`renders components/table/demo/head.tsx extend context correctly 1`] = `
|
||||
tabindex="0"
|
||||
>
|
||||
<div
|
||||
class="ant-table-column-sorters"
|
||||
class="ant-table-column-sorters ant-table-column-sorters-tooltip-target-sorter"
|
||||
>
|
||||
<span
|
||||
class="ant-table-column-title"
|
||||
@ -16500,23 +16500,23 @@ exports[`renders components/table/demo/head.tsx extend context correctly 1`] = `
|
||||
</span>
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
class="ant-tooltip ant-zoom-big-fast-appear ant-zoom-big-fast-appear-prepare ant-zoom-big-fast 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"
|
||||
class="ant-tooltip ant-zoom-big-fast-appear ant-zoom-big-fast-appear-prepare ant-zoom-big-fast ant-tooltip-placement-top"
|
||||
style="--arrow-x: 0px; --arrow-y: 0px; left: -1000vw; top: -1000vh; box-sizing: border-box;"
|
||||
>
|
||||
<div
|
||||
class="ant-tooltip-inner"
|
||||
role="tooltip"
|
||||
class="ant-tooltip-arrow"
|
||||
style="position: absolute; bottom: 0px; left: 0px;"
|
||||
/>
|
||||
<div
|
||||
class="ant-tooltip-content"
|
||||
>
|
||||
Click to cancel sorting
|
||||
<div
|
||||
class="ant-tooltip-inner"
|
||||
role="tooltip"
|
||||
>
|
||||
Click to cancel sorting
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -13204,7 +13204,7 @@ exports[`renders components/table/demo/head.tsx correctly 1`] = `
|
||||
tabindex="0"
|
||||
>
|
||||
<div
|
||||
class="ant-table-column-sorters"
|
||||
class="ant-table-column-sorters ant-table-column-sorters-tooltip-target-sorter"
|
||||
>
|
||||
<span
|
||||
class="ant-table-column-title"
|
||||
|
@ -13,6 +13,7 @@ const columns: TableColumnsType<DataType> = [
|
||||
{
|
||||
title: 'Name',
|
||||
dataIndex: 'name',
|
||||
showSorterTooltip: { target: 'full-header' },
|
||||
filters: [
|
||||
{
|
||||
text: 'Joe',
|
||||
@ -97,6 +98,13 @@ const onChange: TableProps<DataType>['onChange'] = (pagination, filters, sorter,
|
||||
console.log('params', pagination, filters, sorter, extra);
|
||||
};
|
||||
|
||||
const App: React.FC = () => <Table columns={columns} dataSource={data} onChange={onChange} />;
|
||||
const App: React.FC = () => (
|
||||
<Table
|
||||
columns={columns}
|
||||
dataSource={data}
|
||||
onChange={onChange}
|
||||
showSorterTooltip={{ target: 'sorter-icon' }}
|
||||
/>
|
||||
);
|
||||
|
||||
export default App;
|
||||
|
@ -14,6 +14,7 @@ import type {
|
||||
CompareFn,
|
||||
Key,
|
||||
SorterResult,
|
||||
SorterTooltipProps,
|
||||
SortOrder,
|
||||
TableLocale,
|
||||
TransformColumns,
|
||||
@ -111,7 +112,7 @@ function injectSorter<RecordType>(
|
||||
triggerSorter: (sorterSates: SortState<RecordType>) => void,
|
||||
defaultSortDirections: SortOrder[],
|
||||
tableLocale?: TableLocale,
|
||||
tableShowSorterTooltip?: boolean | TooltipProps,
|
||||
tableShowSorterTooltip?: boolean | SorterTooltipProps,
|
||||
pos?: string,
|
||||
): ColumnsType<RecordType> {
|
||||
return (columns || []).map((column, index) => {
|
||||
@ -124,6 +125,7 @@ function injectSorter<RecordType>(
|
||||
newColumn.showSorterTooltip === undefined
|
||||
? tableShowSorterTooltip
|
||||
: newColumn.showSorterTooltip;
|
||||
|
||||
const columnKey = getColumnKey(newColumn, columnPos);
|
||||
const sorterState = sorterStates.find(({ key }) => key === columnKey);
|
||||
const sortOrder = sorterState ? sorterState.sortOrder : null;
|
||||
@ -179,19 +181,35 @@ function injectSorter<RecordType>(
|
||||
...newColumn,
|
||||
className: classNames(newColumn.className, { [`${prefixCls}-column-sort`]: sortOrder }),
|
||||
title: (renderProps: ColumnTitleProps<RecordType>) => {
|
||||
const columnSortersClass = `${prefixCls}-column-sorters`;
|
||||
const renderColumnTitleWrapper = (
|
||||
<span className={`${prefixCls}-column-title`}>
|
||||
{renderColumnTitle(column.title, renderProps)}
|
||||
</span>
|
||||
);
|
||||
const renderSortTitle = (
|
||||
<div className={`${prefixCls}-column-sorters`}>
|
||||
<span className={`${prefixCls}-column-title`}>
|
||||
{renderColumnTitle(column.title, renderProps)}
|
||||
</span>
|
||||
<div className={columnSortersClass}>
|
||||
{renderColumnTitleWrapper}
|
||||
{sorter}
|
||||
</div>
|
||||
);
|
||||
return showSorterTooltip ? (
|
||||
<Tooltip {...tooltipProps}>{renderSortTitle}</Tooltip>
|
||||
) : (
|
||||
renderSortTitle
|
||||
);
|
||||
if (showSorterTooltip) {
|
||||
if (
|
||||
typeof showSorterTooltip !== 'boolean' &&
|
||||
showSorterTooltip?.target === 'sorter-icon'
|
||||
) {
|
||||
return (
|
||||
<div
|
||||
className={`${columnSortersClass} ${prefixCls}-column-sorters-tooltip-target-sorter`}
|
||||
>
|
||||
{renderColumnTitleWrapper}
|
||||
<Tooltip {...tooltipProps}>{sorter}</Tooltip>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return <Tooltip {...tooltipProps}>{renderSortTitle}</Tooltip>;
|
||||
}
|
||||
return renderSortTitle;
|
||||
},
|
||||
onHeaderCell: (col) => {
|
||||
const cell: React.HTMLAttributes<HTMLElement> =
|
||||
@ -357,7 +375,7 @@ interface SorterConfig<RecordType> {
|
||||
) => void;
|
||||
sortDirections: SortOrder[];
|
||||
tableLocale?: TableLocale;
|
||||
showSorterTooltip?: boolean | TooltipProps;
|
||||
showSorterTooltip?: boolean | SorterTooltipProps;
|
||||
}
|
||||
|
||||
export default function useFilterSorter<RecordType>({
|
||||
|
@ -129,19 +129,21 @@ Common props ref:[Common props](/docs/react/common-props)
|
||||
| rowClassName | Row's className | function(record, index): string | - | |
|
||||
| rowKey | Row's unique key, could be a string or function that returns a string | string \| function(record): string | `key` | |
|
||||
| rowSelection | Row selection [config](#rowselection) | object | - | |
|
||||
| rowHoverable | Row hover | boolean | true | 5.16.0 |
|
||||
| scroll | Whether the table can be scrollable, [config](#scroll) | object | - | |
|
||||
| showHeader | Whether to show table header | boolean | true | |
|
||||
| showSorterTooltip | The header show next sorter direction tooltip. It will be set as the property of Tooltip if its type is object | boolean \| [Tooltip props](/components/tooltip/#api) | true | |
|
||||
| showSorterTooltip | The header show next sorter direction tooltip. It will be set as the property of Tooltip if its type is object | boolean \| [Tooltip props](/components/tooltip/#api) & `{target?: 'full-header' \| 'sorter-icon' }` | { target: 'full-header' } | 5.16.0 |
|
||||
| size | Size of table | `large` \| `middle` \| `small` | `large` | |
|
||||
| sortDirections | Supported sort way, could be `ascend`, `descend` | Array | \[`ascend`, `descend`] | |
|
||||
| sticky | Set sticky header and scroll bar | boolean \| `{offsetHeader?: number, offsetScroll?: number, getContainer?: () => HTMLElement}` | - | 4.6.0 (getContainer: 4.7.0) |
|
||||
| summary | Summary content | (currentData) => ReactNode | - | |
|
||||
| tableLayout | The [table-layout](https://developer.mozilla.org/en-US/docs/Web/CSS/table-layout) attribute of table element | - \| `auto` \| `fixed` | -<hr />`fixed` when header/columns are fixed, or using `column.ellipsis` | |
|
||||
| title | Table title renderer | function(currentPageData) | - | |
|
||||
| virtual | Support virtual list | boolean | - | 5.9.0 |
|
||||
| onChange | Callback executed when pagination, filters or sorter is changed | function(pagination, filters, sorter, extra: { currentDataSource: \[], action: `paginate` \| `sort` \| `filter` }) | - | |
|
||||
| onHeaderRow | Set props on per header row | function(columns, index) | - | |
|
||||
| onRow | Set props on per row | function(record, index) | - | |
|
||||
| virtual | Support virtual list | boolean | - | 5.9.0 |
|
||||
| onScroll | Triggered when the table body is scrolled. Note that only vertical scrolling will trigger the event when `virtual` | function(event) | - | 5.16.0 |
|
||||
|
||||
### Table ref
|
||||
|
||||
@ -203,7 +205,7 @@ One of the Table `columns` prop for describing the table's columns, Column has t
|
||||
| responsive | The list of breakpoints at which to display this column. Always visible if not set | [Breakpoint](https://github.com/ant-design/ant-design/blob/015109b42b85c63146371b4e32b883cf97b088e8/components/_util/responsiveObserve.ts#L1)\[] | - | 4.2.0 |
|
||||
| rowScope | Set scope attribute for all cells in this column | `row` \| `rowgroup` | - | 5.1.0 |
|
||||
| shouldCellUpdate | Control cell render logic | (record, prevRecord) => boolean | - | 4.3.0 |
|
||||
| showSorterTooltip | If header show next sorter direction tooltip, override `showSorterTooltip` in table | boolean \| [Tooltip props](/components/tooltip/) | true | |
|
||||
| showSorterTooltip | If header show next sorter direction tooltip, override `showSorterTooltip` in table | boolean \| [Tooltip props](/components/tooltip/) & `{target?: 'full-header' \| 'sorter-icon' }` | { target: 'full-header' } | 5.16.0 |
|
||||
| sortDirections | Supported sort way, override `sortDirections` in `Table`, could be `ascend`, `descend` | Array | \[`ascend`, `descend`] | |
|
||||
| sorter | Sort function for local sort, see [Array.sort](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/sort)'s compareFunction. If it is server-side sorting, set to `true`, but if you want to support multi-column sorting, you can set it to `{ multiple: number }` | function \| boolean \| { compare: function, multiple: number } | - | |
|
||||
| sortOrder | Order of sorted values: `ascend` `descend` `null` | `ascend` \| `descend` \| null | - | |
|
||||
|
@ -130,19 +130,21 @@ const columns = [
|
||||
| rowClassName | 表格行的类名 | function(record, index): string | - | |
|
||||
| rowKey | 表格行 key 的取值,可以是字符串或一个函数 | string \| function(record): string | `key` | |
|
||||
| rowSelection | 表格行是否可选择,[配置项](#rowselection) | object | - | |
|
||||
| rowHoverable | 表格行是否开启 hover 交互 | boolean | true | 5.16.0 |
|
||||
| scroll | 表格是否可滚动,也可以指定滚动区域的宽、高,[配置项](#scroll) | object | - | |
|
||||
| showHeader | 是否显示表头 | boolean | true | |
|
||||
| showSorterTooltip | 表头是否显示下一次排序的 tooltip 提示。当参数类型为对象时,将被设置为 Tooltip 的属性 | boolean \| [Tooltip props](/components/tooltip-cn) | true | |
|
||||
| showSorterTooltip | 表头是否显示下一次排序的 tooltip 提示。当参数类型为对象时,将被设置为 Tooltip 的属性 | boolean \| [Tooltip props](/components/tooltip-cn) & `{target?: 'full-header' \| 'sorter-icon' }` | { target: 'full-header' } | 5.16.0 |
|
||||
| size | 表格大小 | `large` \| `middle` \| `small` | `large` | |
|
||||
| sortDirections | 支持的排序方式,取值为 `ascend` `descend` | Array | \[`ascend`, `descend`] | |
|
||||
| sticky | 设置粘性头部和滚动条 | boolean \| `{offsetHeader?: number, offsetScroll?: number, getContainer?: () => HTMLElement}` | - | 4.6.0 (getContainer: 4.7.0) |
|
||||
| summary | 总结栏 | (currentData) => ReactNode | - | |
|
||||
| tableLayout | 表格元素的 [table-layout](https://developer.mozilla.org/zh-CN/docs/Web/CSS/table-layout) 属性,设为 `fixed` 表示内容不会影响列的布局 | - \| `auto` \| `fixed` | 无<hr />固定表头/列或使用了 `column.ellipsis` 时,默认值为 `fixed` | |
|
||||
| title | 表格标题 | function(currentPageData) | - | |
|
||||
| virtual | 支持虚拟列表 | boolean | - | 5.9.0 |
|
||||
| onChange | 分页、排序、筛选变化时触发 | function(pagination, filters, sorter, extra: { currentDataSource: \[], action: `paginate` \| `sort` \| `filter` }) | - | |
|
||||
| onHeaderRow | 设置头部行属性 | function(columns, index) | - | |
|
||||
| onRow | 设置行属性 | function(record, index) | - | |
|
||||
| virtual | 支持虚拟列表 | boolean | - | 5.9.0 |
|
||||
| onScroll | 表单内容滚动时触发(虚拟滚动下只有垂直滚动会触发事件) | function(event) | - | 5.16.0 |
|
||||
|
||||
### Table ref
|
||||
|
||||
@ -204,7 +206,7 @@ const columns = [
|
||||
| responsive | 响应式 breakpoint 配置列表。未设置则始终可见。 | [Breakpoint](https://github.com/ant-design/ant-design/blob/015109b42b85c63146371b4e32b883cf97b088e8/components/_util/responsiveObserve.ts#L1)\[] | - | 4.2.0 |
|
||||
| rowScope | 设置列范围 | `row` \| `rowgroup` | - | 5.1.0 |
|
||||
| shouldCellUpdate | 自定义单元格渲染时机 | (record, prevRecord) => boolean | - | 4.3.0 |
|
||||
| showSorterTooltip | 表头显示下一次排序的 tooltip 提示, 覆盖 table 中 `showSorterTooltip` | boolean \| [Tooltip props](/components/tooltip-cn/#api) | true | |
|
||||
| showSorterTooltip | 表头显示下一次排序的 tooltip 提示, 覆盖 table 中 `showSorterTooltip` | boolean \| [Tooltip props](/components/tooltip-cn/#api) & `{target?: 'full-header' \| 'sorter-icon' }` | { target: 'full-header' } | 5.16.0 |
|
||||
| sortDirections | 支持的排序方式,覆盖 `Table` 中 `sortDirections`, 取值为 `ascend` `descend` | Array | \[`ascend`, `descend`] | |
|
||||
| sorter | 排序函数,本地排序使用一个函数(参考 [Array.sort](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/sort) 的 compareFunction)。需要服务端排序可设为 `true`(单列排序) 或 `{ multiple: number }`(多列排序) | function \| boolean \| { compare: function, multiple: number } | - | |
|
||||
| sortOrder | 排序的受控属性,外界可用此控制列的排序,可设置为 `ascend` `descend` `null` | `ascend` \| `descend` \| null | - | |
|
||||
|
@ -58,6 +58,12 @@ export interface TableLocale {
|
||||
|
||||
export type SortOrder = 'descend' | 'ascend' | null;
|
||||
|
||||
export type SorterTooltipTarget = 'full-header' | 'sorter-icon';
|
||||
|
||||
export type SorterTooltipProps = TooltipProps & {
|
||||
target?: SorterTooltipTarget;
|
||||
};
|
||||
|
||||
const TableActions = ['paginate', 'sort', 'filter'] as const;
|
||||
export type TableAction = (typeof TableActions)[number];
|
||||
|
||||
@ -123,7 +129,7 @@ export interface ColumnType<RecordType> extends Omit<RcColumnType<RecordType>, '
|
||||
defaultSortOrder?: SortOrder;
|
||||
sortDirections?: SortOrder[];
|
||||
sortIcon?: (props: { sortOrder: SortOrder }) => React.ReactNode;
|
||||
showSorterTooltip?: boolean | TooltipProps;
|
||||
showSorterTooltip?: boolean | SorterTooltipProps;
|
||||
|
||||
// Filter
|
||||
filtered?: boolean;
|
||||
|
@ -1,4 +1,5 @@
|
||||
import type { CSSObject } from '@ant-design/cssinjs';
|
||||
|
||||
import type { GenerateStyle } from '../../theme/internal';
|
||||
import type { TableToken } from './index';
|
||||
|
||||
@ -65,6 +66,12 @@ const genSorterStyle: GenerateStyle<TableToken, CSSObject> = (token) => {
|
||||
},
|
||||
},
|
||||
|
||||
[`${componentCls}-column-sorters-tooltip-target-sorter`]: {
|
||||
'&::after': {
|
||||
content: 'none',
|
||||
},
|
||||
},
|
||||
|
||||
[`${componentCls}-column-sorter`]: {
|
||||
marginInlineStart: marginXXS,
|
||||
color: headerIconColor,
|
||||
|
@ -178,28 +178,25 @@ Array [
|
||||
>
|
||||
Tag 2
|
||||
<span
|
||||
class="ant-tag-close-icon"
|
||||
aria-label="close-circle"
|
||||
class="anticon anticon-close-circle ant-tag-close-icon"
|
||||
role="img"
|
||||
tabindex="-1"
|
||||
>
|
||||
<span
|
||||
aria-label="close-circle"
|
||||
class="anticon anticon-close-circle"
|
||||
role="img"
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
data-icon="close-circle"
|
||||
fill="currentColor"
|
||||
fill-rule="evenodd"
|
||||
focusable="false"
|
||||
height="1em"
|
||||
viewBox="64 64 896 896"
|
||||
width="1em"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
data-icon="close-circle"
|
||||
fill="currentColor"
|
||||
fill-rule="evenodd"
|
||||
focusable="false"
|
||||
height="1em"
|
||||
viewBox="64 64 896 896"
|
||||
width="1em"
|
||||
>
|
||||
<path
|
||||
d="M512 64c247.4 0 448 200.6 448 448S759.4 960 512 960 64 759.4 64 512 264.6 64 512 64zm0 76c-205.4 0-372 166.6-372 372s166.6 372 372 372 372-166.6 372-372-166.6-372-372-372zm128.01 198.83c.03 0 .05.01.09.06l45.02 45.01a.2.2 0 01.05.09.12.12 0 010 .07c0 .02-.01.04-.05.08L557.25 512l127.87 127.86a.27.27 0 01.05.06v.02a.12.12 0 010 .07c0 .03-.01.05-.05.09l-45.02 45.02a.2.2 0 01-.09.05.12.12 0 01-.07 0c-.02 0-.04-.01-.08-.05L512 557.25 384.14 685.12c-.04.04-.06.05-.08.05a.12.12 0 01-.07 0c-.03 0-.05-.01-.09-.05l-45.02-45.02a.2.2 0 01-.05-.09.12.12 0 010-.07c0-.02.01-.04.06-.08L466.75 512 338.88 384.14a.27.27 0 01-.05-.06l-.01-.02a.12.12 0 010-.07c0-.03.01-.05.05-.09l45.02-45.02a.2.2 0 01.09-.05.12.12 0 01.07 0c.02 0 .04.01.08.06L512 466.75l127.86-127.86c.04-.05.06-.06.08-.06a.12.12 0 01.07 0z"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
<path
|
||||
d="M512 64c247.4 0 448 200.6 448 448S759.4 960 512 960 64 759.4 64 512 264.6 64 512 64zm0 76c-205.4 0-372 166.6-372 372s166.6 372 372 372 372-166.6 372-372-166.6-372-372-372zm128.01 198.83c.03 0 .05.01.09.06l45.02 45.01a.2.2 0 01.05.09.12.12 0 010 .07c0 .02-.01.04-.05.08L557.25 512l127.87 127.86a.27.27 0 01.05.06v.02a.12.12 0 010 .07c0 .03-.01.05-.05.09l-45.02 45.02a.2.2 0 01-.09.05.12.12 0 01-.07 0c-.02 0-.04-.01-.08-.05L512 557.25 384.14 685.12c-.04.04-.06.05-.08.05a.12.12 0 01-.07 0c-.03 0-.05-.01-.09-.05l-45.02-45.02a.2.2 0 01-.05-.09.12.12 0 010-.07c0-.02.01-.04.06-.08L466.75 512 338.88 384.14a.27.27 0 01-.05-.06l-.01-.02a.12.12 0 010-.07c0-.03.01-.05.05-.09l45.02-45.02a.2.2 0 01.09-.05.12.12 0 01.07 0c.02 0 .04.01.08.06L512 466.75l127.86-127.86c.04-.05.06-.06.08-.06a.12.12 0 01.07 0z"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
</span>,
|
||||
]
|
||||
@ -845,28 +842,25 @@ exports[`renders components/tag/demo/customize.tsx extend context correctly 1`]
|
||||
>
|
||||
Tag2
|
||||
<span
|
||||
class="ant-tag-close-icon"
|
||||
aria-label="close-circle"
|
||||
class="anticon anticon-close-circle ant-tag-close-icon"
|
||||
role="img"
|
||||
tabindex="-1"
|
||||
>
|
||||
<span
|
||||
aria-label="close-circle"
|
||||
class="anticon anticon-close-circle"
|
||||
role="img"
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
data-icon="close-circle"
|
||||
fill="currentColor"
|
||||
fill-rule="evenodd"
|
||||
focusable="false"
|
||||
height="1em"
|
||||
viewBox="64 64 896 896"
|
||||
width="1em"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
data-icon="close-circle"
|
||||
fill="currentColor"
|
||||
fill-rule="evenodd"
|
||||
focusable="false"
|
||||
height="1em"
|
||||
viewBox="64 64 896 896"
|
||||
width="1em"
|
||||
>
|
||||
<path
|
||||
d="M512 64c247.4 0 448 200.6 448 448S759.4 960 512 960 64 759.4 64 512 264.6 64 512 64zm0 76c-205.4 0-372 166.6-372 372s166.6 372 372 372 372-166.6 372-372-166.6-372-372-372zm128.01 198.83c.03 0 .05.01.09.06l45.02 45.01a.2.2 0 01.05.09.12.12 0 010 .07c0 .02-.01.04-.05.08L557.25 512l127.87 127.86a.27.27 0 01.05.06v.02a.12.12 0 010 .07c0 .03-.01.05-.05.09l-45.02 45.02a.2.2 0 01-.09.05.12.12 0 01-.07 0c-.02 0-.04-.01-.08-.05L512 557.25 384.14 685.12c-.04.04-.06.05-.08.05a.12.12 0 01-.07 0c-.03 0-.05-.01-.09-.05l-45.02-45.02a.2.2 0 01-.05-.09.12.12 0 010-.07c0-.02.01-.04.06-.08L466.75 512 338.88 384.14a.27.27 0 01-.05-.06l-.01-.02a.12.12 0 010-.07c0-.03.01-.05.05-.09l45.02-45.02a.2.2 0 01.09-.05.12.12 0 01.07 0c.02 0 .04.01.08.06L512 466.75l127.86-127.86c.04-.05.06-.06.08-.06a.12.12 0 01.07 0z"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
<path
|
||||
d="M512 64c247.4 0 448 200.6 448 448S759.4 960 512 960 64 759.4 64 512 264.6 64 512 64zm0 76c-205.4 0-372 166.6-372 372s166.6 372 372 372 372-166.6 372-372-166.6-372-372-372zm128.01 198.83c.03 0 .05.01.09.06l45.02 45.01a.2.2 0 01.05.09.12.12 0 010 .07c0 .02-.01.04-.05.08L557.25 512l127.87 127.86a.27.27 0 01.05.06v.02a.12.12 0 010 .07c0 .03-.01.05-.05.09l-45.02 45.02a.2.2 0 01-.09.05.12.12 0 01-.07 0c-.02 0-.04-.01-.08-.05L512 557.25 384.14 685.12c-.04.04-.06.05-.08.05a.12.12 0 01-.07 0c-.03 0-.05-.01-.09-.05l-45.02-45.02a.2.2 0 01-.05-.09.12.12 0 010-.07c0-.02.01-.04.06-.08L466.75 512 338.88 384.14a.27.27 0 01-.05-.06l-.01-.02a.12.12 0 010-.07c0-.03.01-.05.05-.09l45.02-45.02a.2.2 0 01.09-.05.12.12 0 01.07 0c.02 0 .04.01.08.06L512 466.75l127.86-127.86c.04-.05.06-.06.08-.06a.12.12 0 01.07 0z"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
|
@ -176,28 +176,25 @@ Array [
|
||||
>
|
||||
Tag 2
|
||||
<span
|
||||
class="ant-tag-close-icon"
|
||||
aria-label="close-circle"
|
||||
class="anticon anticon-close-circle ant-tag-close-icon"
|
||||
role="img"
|
||||
tabindex="-1"
|
||||
>
|
||||
<span
|
||||
aria-label="close-circle"
|
||||
class="anticon anticon-close-circle"
|
||||
role="img"
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
data-icon="close-circle"
|
||||
fill="currentColor"
|
||||
fill-rule="evenodd"
|
||||
focusable="false"
|
||||
height="1em"
|
||||
viewBox="64 64 896 896"
|
||||
width="1em"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
data-icon="close-circle"
|
||||
fill="currentColor"
|
||||
fill-rule="evenodd"
|
||||
focusable="false"
|
||||
height="1em"
|
||||
viewBox="64 64 896 896"
|
||||
width="1em"
|
||||
>
|
||||
<path
|
||||
d="M512 64c247.4 0 448 200.6 448 448S759.4 960 512 960 64 759.4 64 512 264.6 64 512 64zm0 76c-205.4 0-372 166.6-372 372s166.6 372 372 372 372-166.6 372-372-166.6-372-372-372zm128.01 198.83c.03 0 .05.01.09.06l45.02 45.01a.2.2 0 01.05.09.12.12 0 010 .07c0 .02-.01.04-.05.08L557.25 512l127.87 127.86a.27.27 0 01.05.06v.02a.12.12 0 010 .07c0 .03-.01.05-.05.09l-45.02 45.02a.2.2 0 01-.09.05.12.12 0 01-.07 0c-.02 0-.04-.01-.08-.05L512 557.25 384.14 685.12c-.04.04-.06.05-.08.05a.12.12 0 01-.07 0c-.03 0-.05-.01-.09-.05l-45.02-45.02a.2.2 0 01-.05-.09.12.12 0 010-.07c0-.02.01-.04.06-.08L466.75 512 338.88 384.14a.27.27 0 01-.05-.06l-.01-.02a.12.12 0 010-.07c0-.03.01-.05.05-.09l45.02-45.02a.2.2 0 01.09-.05.12.12 0 01.07 0c.02 0 .04.01.08.06L512 466.75l127.86-127.86c.04-.05.06-.06.08-.06a.12.12 0 01.07 0z"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
<path
|
||||
d="M512 64c247.4 0 448 200.6 448 448S759.4 960 512 960 64 759.4 64 512 264.6 64 512 64zm0 76c-205.4 0-372 166.6-372 372s166.6 372 372 372 372-166.6 372-372-166.6-372-372-372zm128.01 198.83c.03 0 .05.01.09.06l45.02 45.01a.2.2 0 01.05.09.12.12 0 010 .07c0 .02-.01.04-.05.08L557.25 512l127.87 127.86a.27.27 0 01.05.06v.02a.12.12 0 010 .07c0 .03-.01.05-.05.09l-45.02 45.02a.2.2 0 01-.09.05.12.12 0 01-.07 0c-.02 0-.04-.01-.08-.05L512 557.25 384.14 685.12c-.04.04-.06.05-.08.05a.12.12 0 01-.07 0c-.03 0-.05-.01-.09-.05l-45.02-45.02a.2.2 0 01-.05-.09.12.12 0 010-.07c0-.02.01-.04.06-.08L466.75 512 338.88 384.14a.27.27 0 01-.05-.06l-.01-.02a.12.12 0 010-.07c0-.03.01-.05.05-.09l45.02-45.02a.2.2 0 01.09-.05.12.12 0 01.07 0c.02 0 .04.01.08.06L512 466.75l127.86-127.86c.04-.05.06-.06.08-.06a.12.12 0 01.07 0z"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
</span>,
|
||||
]
|
||||
@ -829,28 +826,25 @@ exports[`renders components/tag/demo/customize.tsx correctly 1`] = `
|
||||
>
|
||||
Tag2
|
||||
<span
|
||||
class="ant-tag-close-icon"
|
||||
aria-label="close-circle"
|
||||
class="anticon anticon-close-circle ant-tag-close-icon"
|
||||
role="img"
|
||||
tabindex="-1"
|
||||
>
|
||||
<span
|
||||
aria-label="close-circle"
|
||||
class="anticon anticon-close-circle"
|
||||
role="img"
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
data-icon="close-circle"
|
||||
fill="currentColor"
|
||||
fill-rule="evenodd"
|
||||
focusable="false"
|
||||
height="1em"
|
||||
viewBox="64 64 896 896"
|
||||
width="1em"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
data-icon="close-circle"
|
||||
fill="currentColor"
|
||||
fill-rule="evenodd"
|
||||
focusable="false"
|
||||
height="1em"
|
||||
viewBox="64 64 896 896"
|
||||
width="1em"
|
||||
>
|
||||
<path
|
||||
d="M512 64c247.4 0 448 200.6 448 448S759.4 960 512 960 64 759.4 64 512 264.6 64 512 64zm0 76c-205.4 0-372 166.6-372 372s166.6 372 372 372 372-166.6 372-372-166.6-372-372-372zm128.01 198.83c.03 0 .05.01.09.06l45.02 45.01a.2.2 0 01.05.09.12.12 0 010 .07c0 .02-.01.04-.05.08L557.25 512l127.87 127.86a.27.27 0 01.05.06v.02a.12.12 0 010 .07c0 .03-.01.05-.05.09l-45.02 45.02a.2.2 0 01-.09.05.12.12 0 01-.07 0c-.02 0-.04-.01-.08-.05L512 557.25 384.14 685.12c-.04.04-.06.05-.08.05a.12.12 0 01-.07 0c-.03 0-.05-.01-.09-.05l-45.02-45.02a.2.2 0 01-.05-.09.12.12 0 010-.07c0-.02.01-.04.06-.08L466.75 512 338.88 384.14a.27.27 0 01-.05-.06l-.01-.02a.12.12 0 010-.07c0-.03.01-.05.05-.09l45.02-45.02a.2.2 0 01.09-.05.12.12 0 01.07 0c.02 0 .04.01.08.06L512 466.75l127.86-127.86c.04-.05.06-.06.08-.06a.12.12 0 01.07 0z"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
<path
|
||||
d="M512 64c247.4 0 448 200.6 448 448S759.4 960 512 960 64 759.4 64 512 264.6 64 512 64zm0 76c-205.4 0-372 166.6-372 372s166.6 372 372 372 372-166.6 372-372-166.6-372-372-372zm128.01 198.83c.03 0 .05.01.09.06l45.02 45.01a.2.2 0 01.05.09.12.12 0 010 .07c0 .02-.01.04-.05.08L557.25 512l127.87 127.86a.27.27 0 01.05.06v.02a.12.12 0 010 .07c0 .03-.01.05-.05.09l-45.02 45.02a.2.2 0 01-.09.05.12.12 0 01-.07 0c-.02 0-.04-.01-.08-.05L512 557.25 384.14 685.12c-.04.04-.06.05-.08.05a.12.12 0 01-.07 0c-.03 0-.05-.01-.09-.05l-45.02-45.02a.2.2 0 01-.05-.09.12.12 0 010-.07c0-.02.01-.04.06-.08L466.75 512 338.88 384.14a.27.27 0 01-.05-.06l-.01-.02a.12.12 0 010-.07c0-.03.01-.05.05-.09l45.02-45.02a.2.2 0 01.09-.05.12.12 0 01.07 0c.02 0 .04.01.08.06L512 466.75l127.86-127.86c.04-.05.06-.06.08-.06a.12.12 0 01.07 0z"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
|
@ -1,10 +1,9 @@
|
||||
import React from 'react';
|
||||
import { CheckCircleOutlined } from '@ant-design/icons';
|
||||
import { Simulate } from 'react-dom/test-utils';
|
||||
|
||||
import { CheckCircleOutlined } from '@ant-design/icons';
|
||||
import Tag from '..';
|
||||
import { resetWarned } from '../../_util/warning';
|
||||
|
||||
import mountTest from '../../../tests/shared/mountTest';
|
||||
import rtlTest from '../../../tests/shared/rtlTest';
|
||||
import { act, fireEvent, render } from '../../../tests/utils';
|
||||
@ -219,4 +218,11 @@ describe('Tag', () => {
|
||||
waitRaf();
|
||||
expect(document.querySelector('.ant-wave')).toBeFalsy();
|
||||
});
|
||||
it('should support aria-* in closable', () => {
|
||||
const { container } = render(<Tag closable={{ closeIcon: 'X', 'aria-label': 'CloseBtn' }} />);
|
||||
expect(container.querySelector('.ant-tag-close-icon')?.getAttribute('aria-label')).toEqual(
|
||||
'CloseBtn',
|
||||
);
|
||||
expect(container.querySelector('.ant-tag-close-icon')?.textContent).toEqual('X');
|
||||
});
|
||||
});
|
||||
|
@ -1,10 +1,11 @@
|
||||
import * as React from 'react';
|
||||
import CloseOutlined from '@ant-design/icons/CloseOutlined';
|
||||
import classNames from 'classnames';
|
||||
import omit from 'rc-util/lib/omit';
|
||||
|
||||
import type { PresetColorType, PresetStatusColorType } from '../_util/colors';
|
||||
import { isPresetColor, isPresetStatusColor } from '../_util/colors';
|
||||
import useClosable from '../_util/hooks/useClosable';
|
||||
import useClosable, { pickClosable } from '../_util/hooks/useClosable';
|
||||
import { replaceElement } from '../_util/reactNode';
|
||||
import type { LiteralUnion } from '../_util/type';
|
||||
import { devUseWarning } from '../_util/warning';
|
||||
import Wave from '../_util/wave';
|
||||
@ -21,7 +22,7 @@ export interface TagProps extends React.HTMLAttributes<HTMLSpanElement> {
|
||||
className?: string;
|
||||
rootClassName?: string;
|
||||
color?: LiteralUnion<PresetColorType | PresetStatusColorType>;
|
||||
closable?: boolean;
|
||||
closable?: boolean | ({ closeIcon?: React.ReactNode } & React.AriaAttributes);
|
||||
/** Advised to use closeIcon instead. */
|
||||
closeIcon?: React.ReactNode;
|
||||
/** @deprecated `visible` will be removed in next major version. */
|
||||
@ -47,26 +48,27 @@ const InternalTag: React.ForwardRefRenderFunction<HTMLSpanElement, TagProps> = (
|
||||
icon,
|
||||
color,
|
||||
onClose,
|
||||
closeIcon,
|
||||
closable,
|
||||
bordered = true,
|
||||
visible: deprecatedVisible,
|
||||
...props
|
||||
} = tagProps;
|
||||
const { getPrefixCls, direction, tag } = React.useContext(ConfigContext);
|
||||
const { getPrefixCls, direction, tag: tagContext } = React.useContext(ConfigContext);
|
||||
const [visible, setVisible] = React.useState(true);
|
||||
|
||||
const domProps = omit(props, ['closeIcon', 'closable']);
|
||||
|
||||
// Warning for deprecated usage
|
||||
if (process.env.NODE_ENV !== 'production') {
|
||||
const warning = devUseWarning('Tag');
|
||||
|
||||
warning.deprecated(!('visible' in props), 'visible', 'visible && <Tag />');
|
||||
warning.deprecated(!('visible' in tagProps), 'visible', 'visible && <Tag />');
|
||||
}
|
||||
|
||||
React.useEffect(() => {
|
||||
if ('visible' in props) {
|
||||
setVisible(props.visible!);
|
||||
if (deprecatedVisible !== undefined) {
|
||||
setVisible(deprecatedVisible!);
|
||||
}
|
||||
}, [props.visible]);
|
||||
}, [deprecatedVisible]);
|
||||
|
||||
const isPreset = isPresetColor(color);
|
||||
const isStatus = isPresetStatusColor(color);
|
||||
@ -74,7 +76,7 @@ const InternalTag: React.ForwardRefRenderFunction<HTMLSpanElement, TagProps> = (
|
||||
|
||||
const tagStyle: React.CSSProperties = {
|
||||
backgroundColor: color && !isInternalColor ? color : undefined,
|
||||
...tag?.style,
|
||||
...tagContext?.style,
|
||||
...style,
|
||||
};
|
||||
|
||||
@ -84,7 +86,7 @@ const InternalTag: React.ForwardRefRenderFunction<HTMLSpanElement, TagProps> = (
|
||||
|
||||
const tagClassName = classNames(
|
||||
prefixCls,
|
||||
tag?.className,
|
||||
tagContext?.className,
|
||||
{
|
||||
[`${prefixCls}-${color}`]: isInternalColor,
|
||||
[`${prefixCls}-has-color`]: color && !isInternalColor,
|
||||
@ -108,19 +110,22 @@ const InternalTag: React.ForwardRefRenderFunction<HTMLSpanElement, TagProps> = (
|
||||
setVisible(false);
|
||||
};
|
||||
|
||||
const [, mergedCloseIcon] = useClosable({
|
||||
closable,
|
||||
closeIcon: closeIcon ?? tag?.closeIcon,
|
||||
customCloseIconRender: (iconNode: React.ReactNode) =>
|
||||
iconNode === null ? (
|
||||
<CloseOutlined className={`${prefixCls}-close-icon`} onClick={handleCloseClick} />
|
||||
) : (
|
||||
const [, mergedCloseIcon] = useClosable(pickClosable(tagProps), pickClosable(tagContext), {
|
||||
closable: false,
|
||||
closeIconRender: (iconNode: React.ReactNode) => {
|
||||
const replacement = (
|
||||
<span className={`${prefixCls}-close-icon`} onClick={handleCloseClick}>
|
||||
{iconNode}
|
||||
</span>
|
||||
),
|
||||
defaultCloseIcon: null,
|
||||
defaultClosable: false,
|
||||
);
|
||||
return replaceElement(iconNode, replacement, (originProps) => ({
|
||||
onClick: (e: React.MouseEvent<HTMLElement>) => {
|
||||
originProps?.onClick?.(e);
|
||||
handleCloseClick(e);
|
||||
},
|
||||
className: classNames(originProps?.className, `${prefixCls}-close-icon`),
|
||||
}));
|
||||
},
|
||||
});
|
||||
|
||||
const isNeedWave =
|
||||
@ -139,7 +144,7 @@ const InternalTag: React.ForwardRefRenderFunction<HTMLSpanElement, TagProps> = (
|
||||
);
|
||||
|
||||
const tagNode: React.ReactNode = (
|
||||
<span {...props} ref={ref} className={tagClassName} style={tagStyle}>
|
||||
<span {...domProps} ref={ref} className={tagClassName} style={tagStyle}>
|
||||
{kids}
|
||||
{mergedCloseIcon}
|
||||
{isPreset && <PresetCmp key="preset" prefixCls={prefixCls} />}
|
||||
|
8
components/time-picker/locale/uz_UZ.ts
Normal file
8
components/time-picker/locale/uz_UZ.ts
Normal file
@ -0,0 +1,8 @@
|
||||
import type { TimePickerLocale } from '../index';
|
||||
|
||||
const locale: TimePickerLocale = {
|
||||
placeholder: 'Vaqtni tanlang',
|
||||
rangePlaceholder: ['Boshlanish vaqti', 'Tugallanish vaqti'],
|
||||
};
|
||||
|
||||
export default locale;
|
@ -1,5 +1,4 @@
|
||||
import * as React from 'react';
|
||||
import CloseOutlined from '@ant-design/icons/CloseOutlined';
|
||||
import classNames from 'classnames';
|
||||
|
||||
import useClosable from '../_util/hooks/useClosable';
|
||||
@ -31,17 +30,14 @@ const PurePanel: React.FC<PurePanelProps> = (props) => {
|
||||
|
||||
const [wrapCSSVar, hashId, cssVarCls] = useStyle(prefixCls);
|
||||
|
||||
const [mergedClosable, mergedCloseIcon] = useClosable({
|
||||
closable,
|
||||
closeIcon,
|
||||
customCloseIconRender: (icon) =>
|
||||
const [mergedClosable, mergedCloseIcon] = useClosable({ closable, closeIcon }, null, {
|
||||
closable: true,
|
||||
closeIconRender: (icon) =>
|
||||
React.isValidElement(icon)
|
||||
? cloneElement(icon, {
|
||||
className: classNames(icon.props.className, `${prefixCls}-close-icon`),
|
||||
})
|
||||
: icon,
|
||||
defaultCloseIcon: <CloseOutlined />,
|
||||
defaultClosable: true,
|
||||
});
|
||||
|
||||
return wrapCSSVar(
|
||||
|
@ -4,19 +4,19 @@ import useMergedState from 'rc-util/lib/hooks/useMergedState';
|
||||
|
||||
import type { KeyWiseTransferItem } from '.';
|
||||
import Pagination from '../pagination';
|
||||
import type { PaginationType } from './interface';
|
||||
import type { PaginationType, TransferKey } from './interface';
|
||||
import type { RenderedItem, TransferListProps } from './list';
|
||||
import ListItem from './ListItem';
|
||||
|
||||
export const OmitProps = ['handleFilter', 'handleClear', 'checkedKeys'] as const;
|
||||
export type OmitProp = typeof OmitProps[number];
|
||||
export type OmitProp = (typeof OmitProps)[number];
|
||||
type PartialTransferListProps<RecordType> = Omit<TransferListProps<RecordType>, OmitProp>;
|
||||
type ExistPagination = Exclude<PaginationType, boolean>;
|
||||
|
||||
export interface TransferListBodyProps<RecordType> extends PartialTransferListProps<RecordType> {
|
||||
filteredItems: RecordType[];
|
||||
filteredRenderItems: RenderedItem<RecordType>[];
|
||||
selectedKeys: string[];
|
||||
selectedKeys: TransferKey[];
|
||||
}
|
||||
|
||||
const parsePagination = (pagination?: ExistPagination) => {
|
||||
|
@ -437,7 +437,9 @@ describe('Transfer', () => {
|
||||
const renderFunc: TransferProps<any>['render'] = (item) => item.title;
|
||||
const handleChange = jest.fn();
|
||||
const TransferDemo = () => {
|
||||
const [selectedKeys, setSelectedKeys] = useState<string[]>(searchTransferProps.selectedKeys);
|
||||
const [selectedKeys, setSelectedKeys] = useState<React.Key[]>(
|
||||
searchTransferProps.selectedKeys,
|
||||
);
|
||||
const handleSelectChange: TransferProps<any>['onSelectChange'] = (
|
||||
sourceSelectedKeys,
|
||||
targetSelectedKeys,
|
||||
@ -600,7 +602,7 @@ describe('Transfer', () => {
|
||||
const onSelectChange = jest.fn();
|
||||
|
||||
const Demo = () => {
|
||||
const [selectedKeys, setSelectedKeys] = useState<string[]>([]);
|
||||
const [selectedKeys, setSelectedKeys] = useState<React.Key[]>([]);
|
||||
|
||||
return (
|
||||
<Transfer
|
||||
|
@ -17,7 +17,7 @@ const mockData: RecordType[] = Array.from({ length: 20 }).map((_, i) => ({
|
||||
const initialTargetKeys = mockData.filter((item) => Number(item.key) > 10).map((item) => item.key);
|
||||
|
||||
const App: React.FC = () => {
|
||||
const [targetKeys, setTargetKeys] = useState(initialTargetKeys);
|
||||
const [targetKeys, setTargetKeys] = useState<React.Key[]>(initialTargetKeys);
|
||||
const [selectedKeys, setSelectedKeys] = useState<string[]>([]);
|
||||
|
||||
const onChange: TransferProps['onChange'] = (nextTargetKeys, direction, moveKeys) => {
|
||||
|
@ -114,8 +114,8 @@ const rightTableColumns: TableColumnsType<Pick<DataType, 'title'>> = [
|
||||
const initialTargetKeys = mockData.filter((item) => Number(item.key) > 10).map((item) => item.key);
|
||||
|
||||
const App: React.FC = () => {
|
||||
const [targetKeys, setTargetKeys] = useState(initialTargetKeys);
|
||||
const [selectedKeys, setSelectedKeys] = useState<string[]>([]);
|
||||
const [targetKeys, setTargetKeys] = useState<React.Key[]>(initialTargetKeys);
|
||||
const [selectedKeys, setSelectedKeys] = useState<React.Key[]>([]);
|
||||
|
||||
const onChange: TransferProps['onChange'] = (nextTargetKeys, direction, moveKeys) => {
|
||||
console.log('targetKeys:', nextTargetKeys);
|
||||
|
@ -11,7 +11,7 @@ interface RecordType {
|
||||
|
||||
const App: React.FC = () => {
|
||||
const [mockData, setMockData] = useState<RecordType[]>([]);
|
||||
const [targetKeys, setTargetKeys] = useState<string[]>([]);
|
||||
const [targetKeys, setTargetKeys] = useState<React.Key[]>([]);
|
||||
|
||||
const getMock = () => {
|
||||
const tempTargetKeys = [];
|
||||
|
@ -22,7 +22,7 @@ const selectAllLabels: TransferProps['selectAllLabels'] = [
|
||||
];
|
||||
|
||||
const App: React.FC = () => {
|
||||
const [targetKeys, setTargetKeys] = useState(oriTargetKeys);
|
||||
const [targetKeys, setTargetKeys] = useState<React.Key[]>(oriTargetKeys);
|
||||
return (
|
||||
<Transfer
|
||||
dataSource={mockData}
|
||||
|
@ -12,7 +12,7 @@ interface RecordType {
|
||||
const App: React.FC = () => {
|
||||
const [oneWay, setOneWay] = useState(false);
|
||||
const [mockData, setMockData] = useState<RecordType[]>([]);
|
||||
const [targetKeys, setTargetKeys] = useState<string[]>([]);
|
||||
const [targetKeys, setTargetKeys] = useState<React.Key[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
const newTargetKeys = [];
|
||||
|
@ -19,8 +19,8 @@ const mockData: RecordType[] = Array.from({ length: 20 }).map((_, i) => ({
|
||||
const oriTargetKeys = mockData.filter((item) => Number(item.key) % 3 > 1).map((item) => item.key);
|
||||
|
||||
const App: React.FC = () => {
|
||||
const [targetKeys, setTargetKeys] = useState<string[]>(oriTargetKeys);
|
||||
const [selectedKeys, setSelectedKeys] = useState<string[]>([]);
|
||||
const [targetKeys, setTargetKeys] = useState<React.Key[]>(oriTargetKeys);
|
||||
const [selectedKeys, setSelectedKeys] = useState<React.Key[]>([]);
|
||||
const [disabled, setDisabled] = useState(false);
|
||||
|
||||
const handleChange: TransferProps['onChange'] = (newTargetKeys, direction, moveKeys) => {
|
||||
|
@ -3,11 +3,12 @@ import * as React from 'react';
|
||||
import type { KeyWise, TransferProps } from '..';
|
||||
import { groupKeysMap } from '../../_util/transKeys';
|
||||
import type { AnyObject } from '../../_util/type';
|
||||
import type { TransferKey } from '../interface';
|
||||
|
||||
const useData = <RecordType extends AnyObject>(
|
||||
dataSource?: RecordType[],
|
||||
rowKey?: TransferProps<RecordType>['rowKey'],
|
||||
targetKeys?: string[],
|
||||
targetKeys?: TransferKey[],
|
||||
) => {
|
||||
const mergedDataSource = React.useMemo(
|
||||
() =>
|
||||
|
@ -1,25 +1,26 @@
|
||||
import * as React from 'react';
|
||||
import type { TransferKey } from '../interface';
|
||||
|
||||
const EMPTY_KEYS: string[] = [];
|
||||
const EMPTY_KEYS: TransferKey[] = [];
|
||||
|
||||
function filterKeys(keys: string[], dataKeys: Set<string>) {
|
||||
function filterKeys(keys: TransferKey[], dataKeys: Set<TransferKey>) {
|
||||
const filteredKeys = keys.filter((key) => dataKeys.has(key));
|
||||
return keys.length === filteredKeys.length ? keys : filteredKeys;
|
||||
}
|
||||
|
||||
function flattenKeys(keys: Set<string>) {
|
||||
function flattenKeys(keys: Set<TransferKey>) {
|
||||
return Array.from(keys).join(';');
|
||||
}
|
||||
|
||||
export default function useSelection<T extends { key: string }>(
|
||||
export default function useSelection<T extends { key: TransferKey }>(
|
||||
leftDataSource: T[],
|
||||
rightDataSource: T[],
|
||||
selectedKeys: string[] = EMPTY_KEYS,
|
||||
selectedKeys: TransferKey[] = EMPTY_KEYS,
|
||||
): [
|
||||
sourceSelectedKeys: string[],
|
||||
targetSelectedKeys: string[],
|
||||
setSourceSelectedKeys: React.Dispatch<React.SetStateAction<string[]>>,
|
||||
setTargetSelectedKeys: React.Dispatch<React.SetStateAction<string[]>>,
|
||||
sourceSelectedKeys: TransferKey[],
|
||||
targetSelectedKeys: TransferKey[],
|
||||
setSourceSelectedKeys: React.Dispatch<React.SetStateAction<TransferKey[]>>,
|
||||
setTargetSelectedKeys: React.Dispatch<React.SetStateAction<TransferKey[]>>,
|
||||
] {
|
||||
// Prepare `dataSource` keys
|
||||
const [leftKeys, rightKeys] = React.useMemo(
|
||||
|
@ -52,11 +52,11 @@ Common props ref:[Common props](/docs/react/common-props)
|
||||
| pagination | Use pagination. Not work in render props | boolean \| { pageSize: number, simple: boolean, showSizeChanger?: boolean, showLessItems?: boolean } | false | 4.3.0 |
|
||||
| render | The function to generate the item shown on a column. Based on an record (element of the dataSource array), this function should return a React element which is generated from that record. Also, it can return a plain object with `value` and `label`, `label` is a React element and `value` is for title | (record) => ReactNode | - | |
|
||||
| selectAllLabels | A set of customized labels for select all checkboxes on the header | (ReactNode \| (info: { selectedCount: number, totalCount: number }) => ReactNode)\[] | - | |
|
||||
| selectedKeys | A set of keys of selected items | string\[] | \[] | |
|
||||
| selectedKeys | A set of keys of selected items | string\[] \| number\[] | \[] | |
|
||||
| showSearch | If included, a search box is shown on each column | boolean | false | |
|
||||
| showSelectAll | Show select all checkbox on the header | boolean | true | |
|
||||
| status | Set validation status | 'error' \| 'warning' | - | 4.19.0 |
|
||||
| targetKeys | A set of keys of elements that are listed on the right column | string\[] | \[] | |
|
||||
| targetKeys | A set of keys of elements that are listed on the right column | string\[] \| number\[] | \[] | |
|
||||
| titles | A set of titles that are sorted from left to right | ReactNode\[] | - | |
|
||||
| onChange | A callback function that is executed when the transfer between columns is complete | (targetKeys, direction, moveKeys): void | - | |
|
||||
| onScroll | A callback function which is executed when scroll options list | (direction, event): void | - | |
|
||||
@ -67,14 +67,14 @@ Common props ref:[Common props](/docs/react/common-props)
|
||||
|
||||
Transfer accept `children` to customize render list, using follow props:
|
||||
|
||||
| Property | Description | Type | Version |
|
||||
| --------------- | ----------------------- | ------------------------------------ | ------- |
|
||||
| direction | List render direction | `left` \| `right` | |
|
||||
| disabled | Disable list or not | boolean | |
|
||||
| filteredItems | Filtered items | RecordType\[] | |
|
||||
| selectedKeys | Selected items | string\[] | |
|
||||
| onItemSelect | Select item | (key: string, selected: boolean) | |
|
||||
| onItemSelectAll | Select a group of items | (keys: string\[], selected: boolean) | |
|
||||
| Property | Description | Type | Version |
|
||||
| --- | --- | --- | --- |
|
||||
| direction | List render direction | `left` \| `right` | |
|
||||
| disabled | Disable list or not | boolean | |
|
||||
| filteredItems | Filtered items | RecordType\[] | |
|
||||
| selectedKeys | Selected items | string\[] \| number\[] | |
|
||||
| onItemSelect | Select item | (key: string \| number, selected: boolean) | |
|
||||
| onItemSelectAll | Select a group of items | (keys: string\[] \| number\[], selected: boolean) | |
|
||||
|
||||
#### example
|
||||
|
||||
|
@ -17,7 +17,7 @@ import { useLocale } from '../locale';
|
||||
import defaultLocale from '../locale/en_US';
|
||||
import useData from './hooks/useData';
|
||||
import useSelection from './hooks/useSelection';
|
||||
import type { PaginationType } from './interface';
|
||||
import type { PaginationType, TransferKey } from './interface';
|
||||
import type { TransferCustomListBodyProps, TransferListProps } from './list';
|
||||
import List from './list';
|
||||
import Operation from './operation';
|
||||
@ -38,14 +38,14 @@ export interface RenderResultObject {
|
||||
export type RenderResult = React.ReactElement | RenderResultObject | string | null;
|
||||
|
||||
export interface TransferItem {
|
||||
key?: string;
|
||||
key?: TransferKey;
|
||||
title?: string;
|
||||
description?: string;
|
||||
disabled?: boolean;
|
||||
[name: string]: any;
|
||||
}
|
||||
|
||||
export type KeyWise<T> = T & { key: string };
|
||||
export type KeyWise<T> = T & { key: TransferKey };
|
||||
|
||||
export type KeyWiseTransferItem = KeyWise<TransferItem>;
|
||||
|
||||
@ -79,11 +79,15 @@ export interface TransferProps<RecordType = any> {
|
||||
rootClassName?: string;
|
||||
disabled?: boolean;
|
||||
dataSource?: RecordType[];
|
||||
targetKeys?: string[];
|
||||
selectedKeys?: string[];
|
||||
targetKeys?: TransferKey[];
|
||||
selectedKeys?: TransferKey[];
|
||||
render?: TransferRender<RecordType>;
|
||||
onChange?: (targetKeys: string[], direction: TransferDirection, moveKeys: string[]) => void;
|
||||
onSelectChange?: (sourceSelectedKeys: string[], targetSelectedKeys: string[]) => void;
|
||||
onChange?: (
|
||||
targetKeys: TransferKey[],
|
||||
direction: TransferDirection,
|
||||
moveKeys: TransferKey[],
|
||||
) => void;
|
||||
onSelectChange?: (sourceSelectedKeys: TransferKey[], targetSelectedKeys: TransferKey[]) => void;
|
||||
style?: React.CSSProperties;
|
||||
listStyle?: ((style: ListStyle) => CSSProperties) | CSSProperties;
|
||||
operationStyle?: CSSProperties;
|
||||
@ -96,7 +100,7 @@ export interface TransferProps<RecordType = any> {
|
||||
props: TransferListProps<RecordType>,
|
||||
info?: { direction: TransferDirection },
|
||||
) => React.ReactNode;
|
||||
rowKey?: (record: RecordType) => string;
|
||||
rowKey?: (record: RecordType) => TransferKey;
|
||||
onSearch?: (direction: TransferDirection, value: string) => void;
|
||||
onScroll?: (direction: TransferDirection, e: React.SyntheticEvent<HTMLUListElement>) => void;
|
||||
children?: (props: TransferCustomListBodyProps<RecordType>) => React.ReactNode;
|
||||
@ -172,11 +176,11 @@ const Transfer = <RecordType extends TransferItem = TransferItem>(
|
||||
|
||||
const [leftMultipleSelect, updateLeftPrevSelectedIndex] = useMultipleSelect<
|
||||
KeyWise<RecordType>,
|
||||
string
|
||||
TransferKey
|
||||
>((item) => item.key);
|
||||
const [rightMultipleSelect, updateRightPrevSelectedIndex] = useMultipleSelect<
|
||||
KeyWise<RecordType>,
|
||||
string
|
||||
TransferKey
|
||||
>((item) => item.key);
|
||||
|
||||
if (process.env.NODE_ENV !== 'production') {
|
||||
@ -186,7 +190,10 @@ const Transfer = <RecordType extends TransferItem = TransferItem>(
|
||||
}
|
||||
|
||||
const setStateKeys = useCallback(
|
||||
(direction: TransferDirection, keys: string[] | ((prevKeys: string[]) => string[])) => {
|
||||
(
|
||||
direction: TransferDirection,
|
||||
keys: TransferKey[] | ((prevKeys: TransferKey[]) => TransferKey[]),
|
||||
) => {
|
||||
if (direction === 'left') {
|
||||
const nextKeys = typeof keys === 'function' ? keys(sourceSelectedKeys || []) : keys;
|
||||
setSourceSelectedKeys(nextKeys);
|
||||
@ -207,7 +214,7 @@ const Transfer = <RecordType extends TransferItem = TransferItem>(
|
||||
};
|
||||
|
||||
const handleSelectChange = useCallback(
|
||||
(direction: TransferDirection, holder: string[]) => {
|
||||
(direction: TransferDirection, holder: TransferKey[]) => {
|
||||
if (direction === 'left') {
|
||||
onSelectChange?.(holder, targetSelectedKeys);
|
||||
} else {
|
||||
@ -263,12 +270,12 @@ const Transfer = <RecordType extends TransferItem = TransferItem>(
|
||||
checkAll: boolean | 'replace',
|
||||
) => {
|
||||
setStateKeys(direction, (prevKeys) => {
|
||||
let mergedCheckedKeys: string[] = [];
|
||||
let mergedCheckedKeys: TransferKey[] = [];
|
||||
if (checkAll === 'replace') {
|
||||
mergedCheckedKeys = keys;
|
||||
} else if (checkAll) {
|
||||
// Merge current keys with origin key
|
||||
mergedCheckedKeys = Array.from(new Set<string>([...prevKeys, ...keys]));
|
||||
mergedCheckedKeys = Array.from(new Set<TransferKey>([...prevKeys, ...keys]));
|
||||
} else {
|
||||
const selectedKeysMap = groupKeysMap(keys);
|
||||
// Remove current keys from origin keys
|
||||
@ -298,8 +305,8 @@ const Transfer = <RecordType extends TransferItem = TransferItem>(
|
||||
|
||||
const handleSingleSelect = (
|
||||
direction: TransferDirection,
|
||||
holder: Set<string>,
|
||||
selectedKey: string,
|
||||
holder: Set<TransferKey>,
|
||||
selectedKey: TransferKey,
|
||||
checked: boolean,
|
||||
currentSelectedIndex: number,
|
||||
) => {
|
||||
@ -317,7 +324,7 @@ const Transfer = <RecordType extends TransferItem = TransferItem>(
|
||||
const handleMultipleSelect = (
|
||||
direction: TransferDirection,
|
||||
data: KeyWise<RecordType>[],
|
||||
holder: Set<string>,
|
||||
holder: Set<TransferKey>,
|
||||
currentSelectedIndex: number,
|
||||
) => {
|
||||
const isLeftDirection = direction === 'left';
|
||||
@ -327,7 +334,7 @@ const Transfer = <RecordType extends TransferItem = TransferItem>(
|
||||
|
||||
const onItemSelect = (
|
||||
direction: TransferDirection,
|
||||
selectedKey: string,
|
||||
selectedKey: TransferKey,
|
||||
checked: boolean,
|
||||
multiple?: boolean,
|
||||
) => {
|
||||
@ -360,14 +367,14 @@ const Transfer = <RecordType extends TransferItem = TransferItem>(
|
||||
};
|
||||
|
||||
const onRightItemSelect = (
|
||||
selectedKey: string,
|
||||
selectedKey: TransferKey,
|
||||
checked: boolean,
|
||||
e?: React.MouseEvent<Element, MouseEvent>,
|
||||
) => {
|
||||
onItemSelect('right', selectedKey, checked, e?.shiftKey);
|
||||
};
|
||||
|
||||
const onRightItemRemove = (keys: string[]) => {
|
||||
const onRightItemRemove = (keys: TransferKey[]) => {
|
||||
setStateKeys('right', []);
|
||||
onChange?.(
|
||||
targetKeys.filter((key) => !keys.includes(key)),
|
||||
|
@ -55,11 +55,11 @@ coverDark: https://mdn.alipayobjects.com/huamei_7uahnr/afts/img/A*g9vUQq2nkpEAAA
|
||||
| pagination | 使用分页样式,自定义渲染列表下无效 | boolean \| { pageSize: number, simple: boolean, showSizeChanger?: boolean, showLessItems?: boolean } | false | 4.3.0 |
|
||||
| render | 每行数据渲染函数,该函数的入参为 `dataSource` 中的项,返回值为 ReactElement。或者返回一个普通对象,其中 `label` 字段为 ReactElement,`value` 字段为 title | (record) => ReactNode | - | |
|
||||
| selectAllLabels | 自定义顶部多选框标题的集合 | (ReactNode \| (info: { selectedCount: number, totalCount: number }) => ReactNode)\[] | - | |
|
||||
| selectedKeys | 设置哪些项应该被选中 | string\[] | \[] | |
|
||||
| selectedKeys | 设置哪些项应该被选中 | string\[] \| number\[] | \[] | |
|
||||
| showSearch | 是否显示搜索框 | boolean | false | |
|
||||
| showSelectAll | 是否展示全选勾选框 | boolean | true | |
|
||||
| status | 设置校验状态 | 'error' \| 'warning' | - | 4.19.0 |
|
||||
| targetKeys | 显示在右侧框数据的 key 集合 | string\[] | \[] | |
|
||||
| targetKeys | 显示在右侧框数据的 key 集合 | string\[] \| number\[] | \[] | |
|
||||
| titles | 标题集合,顺序从左至右 | ReactNode\[] | - | |
|
||||
| onChange | 选项在两栏之间转移时的回调函数 | (targetKeys, direction, moveKeys): void | - | |
|
||||
| onScroll | 选项列表滚动时的回调函数 | (direction, event): void | - | |
|
||||
@ -70,14 +70,14 @@ coverDark: https://mdn.alipayobjects.com/huamei_7uahnr/afts/img/A*g9vUQq2nkpEAAA
|
||||
|
||||
Transfer 支持接收 `children` 自定义渲染列表,并返回以下参数:
|
||||
|
||||
| 参数 | 说明 | 类型 | 版本 |
|
||||
| --------------- | -------------- | ------------------------------------ | ---- |
|
||||
| direction | 渲染列表的方向 | `left` \| `right` | |
|
||||
| disabled | 是否禁用列表 | boolean | |
|
||||
| filteredItems | 过滤后的数据 | RecordType\[] | |
|
||||
| selectedKeys | 选中的条目 | string\[] | |
|
||||
| onItemSelect | 勾选条目 | (key: string, selected: boolean) | |
|
||||
| onItemSelectAll | 勾选一组条目 | (keys: string\[], selected: boolean) | |
|
||||
| 参数 | 说明 | 类型 | 版本 |
|
||||
| --------------- | -------------- | ------------------------------------------------- | ---- |
|
||||
| direction | 渲染列表的方向 | `left` \| `right` | |
|
||||
| disabled | 是否禁用列表 | boolean | |
|
||||
| filteredItems | 过滤后的数据 | RecordType\[] | |
|
||||
| selectedKeys | 选中的条目 | string\[] \| number\[] | |
|
||||
| onItemSelect | 勾选条目 | (key: string \| number, selected: boolean) | |
|
||||
| onItemSelectAll | 勾选一组条目 | (keys: string\[] \| number\[], selected: boolean) | |
|
||||
|
||||
#### 参考示例
|
||||
|
||||
|
@ -1,3 +1,5 @@
|
||||
export type TransferKey = React.Key;
|
||||
|
||||
export type PaginationType =
|
||||
| boolean
|
||||
| {
|
||||
|
@ -15,7 +15,7 @@ import type {
|
||||
TransferDirection,
|
||||
TransferLocale,
|
||||
} from './index';
|
||||
import type { PaginationType } from './interface';
|
||||
import type { PaginationType, TransferKey } from './interface';
|
||||
import type { ListBodyRef, TransferListBodyProps } from './ListBody';
|
||||
import DefaultListBody, { OmitProps } from './ListBody';
|
||||
import Search from './search';
|
||||
@ -50,11 +50,15 @@ export interface TransferListProps<RecordType> extends TransferLocale {
|
||||
dataSource: RecordType[];
|
||||
filterOption?: (filterText: string, item: RecordType, direction: TransferDirection) => boolean;
|
||||
style?: React.CSSProperties;
|
||||
checkedKeys: string[];
|
||||
checkedKeys: TransferKey[];
|
||||
handleFilter: (e: React.ChangeEvent<HTMLInputElement>) => void;
|
||||
onItemSelect: (key: string, check: boolean, e?: React.MouseEvent<Element, MouseEvent>) => void;
|
||||
onItemSelectAll: (dataSource: string[], checkAll: boolean | 'replace') => void;
|
||||
onItemRemove?: (keys: string[]) => void;
|
||||
onItemSelect: (
|
||||
key: TransferKey,
|
||||
check: boolean,
|
||||
e?: React.MouseEvent<Element, MouseEvent>,
|
||||
) => void;
|
||||
onItemSelectAll: (dataSource: TransferKey[], checkAll: boolean | 'replace') => void;
|
||||
onItemRemove?: (keys: TransferKey[]) => void;
|
||||
handleClear: () => void;
|
||||
/** Render item */
|
||||
render?: (item: RecordType) => RenderResult;
|
||||
|
@ -1,4 +1,5 @@
|
||||
import * as React from 'react';
|
||||
import LoadingOutlined from '@ant-design/icons/LoadingOutlined';
|
||||
import CheckOutlined from '@ant-design/icons/CheckOutlined';
|
||||
import CopyOutlined from '@ant-design/icons/CopyOutlined';
|
||||
import classNames from 'classnames';
|
||||
@ -15,10 +16,11 @@ export interface CopyBtnProps extends CopyConfig {
|
||||
locale: Locale['Text'];
|
||||
onCopy: React.MouseEventHandler<HTMLDivElement>;
|
||||
iconOnly: boolean;
|
||||
loading: boolean;
|
||||
}
|
||||
|
||||
export default function CopyBtn(props: CopyBtnProps) {
|
||||
const { prefixCls, copied, locale = {}, onCopy, iconOnly, tooltips, icon } = props;
|
||||
const { prefixCls, copied, locale = {}, onCopy, iconOnly, tooltips, icon, loading } = props;
|
||||
|
||||
const tooltipNodes = toList(tooltips);
|
||||
const iconNodes = toList(icon);
|
||||
@ -43,7 +45,7 @@ export default function CopyBtn(props: CopyBtnProps) {
|
||||
>
|
||||
{copied
|
||||
? getNode(iconNodes[1], <CheckOutlined />, true)
|
||||
: getNode(iconNodes[0], <CopyOutlined />, true)}
|
||||
: getNode(iconNodes[0], loading ? <LoadingOutlined /> : <CopyOutlined />, true)}
|
||||
</TransButton>
|
||||
</Tooltip>
|
||||
);
|
||||
|
@ -105,12 +105,11 @@ export interface EllipsisProps {
|
||||
rows: number;
|
||||
children: (
|
||||
cutChildren: React.ReactNode[],
|
||||
/** Tell current `cutChildren` is in ellipsis */
|
||||
inEllipsis: boolean,
|
||||
/** Tell current `text` is exceed the `rows` which can be ellipsis */
|
||||
canEllipsis: boolean,
|
||||
) => React.ReactNode;
|
||||
onEllipsis: (isEllipsis: boolean) => void;
|
||||
expanded: boolean;
|
||||
/**
|
||||
* Mark for misc update. Which will not affect ellipsis content length.
|
||||
* e.g. tooltip content update.
|
||||
@ -131,13 +130,14 @@ const lineClipStyle: React.CSSProperties = {
|
||||
};
|
||||
|
||||
export default function EllipsisMeasure(props: EllipsisProps) {
|
||||
const { enableMeasure, width, text, children, rows, miscDeps, onEllipsis } = props;
|
||||
const { enableMeasure, width, text, children, rows, expanded, miscDeps, onEllipsis } = props;
|
||||
|
||||
const nodeList = React.useMemo(() => toArray(text), [text]);
|
||||
const nodeLen = React.useMemo(() => getNodesLen(nodeList), [text]);
|
||||
|
||||
// ========================= Full Content =========================
|
||||
const fullContent = React.useMemo(() => children(nodeList, false, false), [text]);
|
||||
// Used for measure only, which means it's always render as no need ellipsis
|
||||
const fullContent = React.useMemo(() => children(nodeList, false), [text]);
|
||||
|
||||
// ========================= Cut Content ==========================
|
||||
const [ellipsisCutIndex, setEllipsisCutIndex] = React.useState<[number, number] | null>(null);
|
||||
@ -150,6 +150,7 @@ export default function EllipsisMeasure(props: EllipsisProps) {
|
||||
const descRowsEllipsisRef = React.useRef<MeasureTextRef>(null);
|
||||
const symbolRowEllipsisRef = React.useRef<MeasureTextRef>(null);
|
||||
|
||||
const [canEllipsis, setCanEllipsis] = React.useState(false);
|
||||
const [needEllipsis, setNeedEllipsis] = React.useState(STATUS_MEASURE_NONE);
|
||||
const [ellipsisHeight, setEllipsisHeight] = React.useState(0);
|
||||
|
||||
@ -169,6 +170,7 @@ export default function EllipsisMeasure(props: EllipsisProps) {
|
||||
|
||||
setNeedEllipsis(isOverflow ? STATUS_MEASURE_NEED_ELLIPSIS : STATUS_MEASURE_NO_NEED_ELLIPSIS);
|
||||
setEllipsisCutIndex(isOverflow ? [0, nodeLen] : null);
|
||||
setCanEllipsis(isOverflow);
|
||||
|
||||
// Get the basic height of ellipsis rows
|
||||
const baseRowsEllipsisHeight = needEllipsisRef.current?.getHeight() || 0;
|
||||
@ -218,7 +220,7 @@ export default function EllipsisMeasure(props: EllipsisProps) {
|
||||
!ellipsisCutIndex ||
|
||||
ellipsisCutIndex[0] !== ellipsisCutIndex[1]
|
||||
) {
|
||||
const content = children(nodeList, false, false);
|
||||
const content = children(nodeList, false);
|
||||
|
||||
// Limit the max line count to avoid scrollbar blink
|
||||
// https://github.com/ant-design/ant-design/issues/42958
|
||||
@ -241,8 +243,8 @@ export default function EllipsisMeasure(props: EllipsisProps) {
|
||||
return content;
|
||||
}
|
||||
|
||||
return children(sliceNodes(nodeList, ellipsisCutIndex[0]), true, true);
|
||||
}, [needEllipsis, ellipsisCutIndex, nodeList, ...miscDeps]);
|
||||
return children(expanded ? nodeList : sliceNodes(nodeList, ellipsisCutIndex[0]), canEllipsis);
|
||||
}, [expanded, needEllipsis, ellipsisCutIndex, nodeList, ...miscDeps]);
|
||||
|
||||
// ============================ Render ============================
|
||||
const measureStyle: React.CSSProperties = {
|
||||
@ -293,7 +295,7 @@ export default function EllipsisMeasure(props: EllipsisProps) {
|
||||
}}
|
||||
ref={symbolRowEllipsisRef}
|
||||
>
|
||||
{children([], true, true)}
|
||||
{children([], true)}
|
||||
</MeasureText>
|
||||
</>
|
||||
)}
|
||||
@ -309,7 +311,7 @@ export default function EllipsisMeasure(props: EllipsisProps) {
|
||||
}}
|
||||
ref={cutMidRef}
|
||||
>
|
||||
{children(sliceNodes(nodeList, cutMidIndex), true, true)}
|
||||
{children(sliceNodes(nodeList, cutMidIndex), true)}
|
||||
</MeasureText>
|
||||
)}
|
||||
</>
|
||||
|
@ -1,7 +1,6 @@
|
||||
import * as React from 'react';
|
||||
import EditOutlined from '@ant-design/icons/EditOutlined';
|
||||
import classNames from 'classnames';
|
||||
import copy from 'copy-to-clipboard';
|
||||
import ResizeObserver from 'rc-resize-observer';
|
||||
import type { AutoSizeType } from 'rc-textarea';
|
||||
import toArray from 'rc-util/lib/Children/toArray';
|
||||
@ -17,6 +16,7 @@ import useLocale from '../../locale/useLocale';
|
||||
import type { TooltipProps } from '../../tooltip';
|
||||
import Tooltip from '../../tooltip';
|
||||
import Editable from '../Editable';
|
||||
import useCopyClick from '../hooks/useCopyClick';
|
||||
import useMergedConfig from '../hooks/useMergedConfig';
|
||||
import useUpdatedEffect from '../hooks/useUpdatedEffect';
|
||||
import type { TypographyProps } from '../Typography';
|
||||
@ -28,7 +28,7 @@ import EllipsisTooltip from './EllipsisTooltip';
|
||||
export type BaseType = 'secondary' | 'success' | 'warning' | 'danger';
|
||||
|
||||
export interface CopyConfig {
|
||||
text?: string;
|
||||
text?: string | (() => string | Promise<string>);
|
||||
onCopy?: (event?: React.MouseEvent<HTMLDivElement>) => void;
|
||||
icon?: React.ReactNode;
|
||||
tooltips?: React.ReactNode;
|
||||
@ -52,10 +52,12 @@ interface EditConfig {
|
||||
|
||||
export interface EllipsisConfig {
|
||||
rows?: number;
|
||||
expandable?: boolean;
|
||||
expandable?: boolean | 'collapsible';
|
||||
suffix?: string;
|
||||
symbol?: React.ReactNode;
|
||||
onExpand?: React.MouseEventHandler<HTMLElement>;
|
||||
symbol?: React.ReactNode | ((expanded: boolean) => React.ReactNode);
|
||||
defaultExpanded?: boolean;
|
||||
expanded?: boolean;
|
||||
onExpand?: (e: React.MouseEvent<HTMLElement, MouseEvent>, info: { expanded: boolean }) => void;
|
||||
onEllipsis?: (ellipsis: boolean) => void;
|
||||
tooltip?: React.ReactNode | TooltipProps;
|
||||
}
|
||||
@ -178,52 +180,26 @@ const Base = React.forwardRef<HTMLElement, BlockProps>((props, ref) => {
|
||||
|
||||
// ========================== Copyable ==========================
|
||||
const [enableCopy, copyConfig] = useMergedConfig<CopyConfig>(copyable);
|
||||
const [copied, setCopied] = React.useState(false);
|
||||
const copyIdRef = React.useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||
|
||||
const copyOptions: Pick<CopyConfig, 'format'> = {};
|
||||
if (copyConfig.format) {
|
||||
copyOptions.format = copyConfig.format;
|
||||
}
|
||||
|
||||
const cleanCopyId = () => {
|
||||
if (copyIdRef.current) {
|
||||
clearTimeout(copyIdRef.current);
|
||||
}
|
||||
};
|
||||
|
||||
const onCopyClick = (e?: React.MouseEvent<HTMLDivElement>) => {
|
||||
e?.preventDefault();
|
||||
e?.stopPropagation();
|
||||
|
||||
copy(copyConfig.text || String(children) || '', copyOptions);
|
||||
|
||||
setCopied(true);
|
||||
|
||||
// Trigger tips update
|
||||
cleanCopyId();
|
||||
copyIdRef.current = setTimeout(() => {
|
||||
setCopied(false);
|
||||
}, 3000);
|
||||
|
||||
copyConfig.onCopy?.(e);
|
||||
};
|
||||
|
||||
React.useEffect(() => cleanCopyId, []);
|
||||
const { copied, copyLoading, onClick: onCopyClick } = useCopyClick({ copyConfig, children });
|
||||
|
||||
// ========================== Ellipsis ==========================
|
||||
const [isLineClampSupport, setIsLineClampSupport] = React.useState(false);
|
||||
const [isTextOverflowSupport, setIsTextOverflowSupport] = React.useState(false);
|
||||
|
||||
const [expanded, setExpanded] = React.useState(false);
|
||||
const [isJsEllipsis, setIsJsEllipsis] = React.useState(false);
|
||||
const [isNativeEllipsis, setIsNativeEllipsis] = React.useState(false);
|
||||
const [isNativeVisible, setIsNativeVisible] = React.useState(true);
|
||||
const [enableEllipsis, ellipsisConfig] = useMergedConfig<EllipsisConfig>(ellipsis, {
|
||||
expandable: false,
|
||||
symbol: (isExpanded) => (isExpanded ? textLocale?.collapse : textLocale?.expand),
|
||||
});
|
||||
const [expanded, setExpanded] = useMergedState(ellipsisConfig.defaultExpanded || false, {
|
||||
value: ellipsisConfig.expanded,
|
||||
});
|
||||
|
||||
const mergedEnableEllipsis = enableEllipsis && !expanded;
|
||||
const mergedEnableEllipsis =
|
||||
enableEllipsis && (!expanded || ellipsisConfig.expandable === 'collapsible');
|
||||
|
||||
// Shared prop to reduce bundle size
|
||||
const { rows = 1 } = ellipsisConfig;
|
||||
@ -267,9 +243,9 @@ const Base = React.forwardRef<HTMLElement, BlockProps>((props, ref) => {
|
||||
const cssLineClamp = mergedEnableEllipsis && rows > 1 && cssEllipsis;
|
||||
|
||||
// >>>>> Expand
|
||||
const onExpandClick: React.MouseEventHandler<HTMLElement> = (e) => {
|
||||
setExpanded(true);
|
||||
ellipsisConfig.onExpand?.(e);
|
||||
const onExpandClick: EllipsisConfig['onExpand'] = (e, info) => {
|
||||
setExpanded(info.expanded);
|
||||
ellipsisConfig.onExpand?.(e, info);
|
||||
};
|
||||
|
||||
const [ellipsisWidth, setEllipsisWidth] = React.useState(0);
|
||||
@ -389,22 +365,16 @@ const Base = React.forwardRef<HTMLElement, BlockProps>((props, ref) => {
|
||||
const { expandable, symbol } = ellipsisConfig;
|
||||
|
||||
if (!expandable) return null;
|
||||
|
||||
let expandContent: React.ReactNode;
|
||||
if (symbol) {
|
||||
expandContent = symbol;
|
||||
} else {
|
||||
expandContent = textLocale?.expand;
|
||||
}
|
||||
if (expanded && expandable !== 'collapsible') return null;
|
||||
|
||||
return (
|
||||
<a
|
||||
key="expand"
|
||||
className={`${prefixCls}-expand`}
|
||||
onClick={onExpandClick}
|
||||
aria-label={textLocale?.expand}
|
||||
className={`${prefixCls}-${expanded ? 'collapse' : 'expand'}`}
|
||||
onClick={(e) => onExpandClick(e, { expanded: !expanded })}
|
||||
aria-label={expanded ? textLocale.collapse : textLocale?.expand}
|
||||
>
|
||||
{expandContent}
|
||||
{typeof symbol === 'function' ? symbol(expanded) : symbol}
|
||||
</a>
|
||||
);
|
||||
};
|
||||
@ -446,25 +416,27 @@ const Base = React.forwardRef<HTMLElement, BlockProps>((props, ref) => {
|
||||
copied={copied}
|
||||
locale={textLocale}
|
||||
onCopy={onCopyClick}
|
||||
loading={copyLoading}
|
||||
iconOnly={children === null || children === undefined}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const renderOperations = (renderExpanded: boolean) => [
|
||||
renderExpanded && renderExpand(),
|
||||
const renderOperations = (canEllipsis: boolean) => [
|
||||
// (renderExpanded || ellipsisConfig.collapsible) && renderExpand(),
|
||||
canEllipsis && renderExpand(),
|
||||
renderEdit(),
|
||||
renderCopy(),
|
||||
];
|
||||
|
||||
const renderEllipsis = (needEllipsis: boolean) => [
|
||||
needEllipsis && (
|
||||
const renderEllipsis = (canEllipsis: boolean) => [
|
||||
canEllipsis && !expanded && (
|
||||
<span aria-hidden key="ellipsis">
|
||||
{ELLIPSIS_STR}
|
||||
</span>
|
||||
),
|
||||
ellipsisConfig.suffix,
|
||||
renderOperations(needEllipsis),
|
||||
renderOperations(canEllipsis),
|
||||
];
|
||||
|
||||
return (
|
||||
@ -506,11 +478,12 @@ const Base = React.forwardRef<HTMLElement, BlockProps>((props, ref) => {
|
||||
rows={rows}
|
||||
width={ellipsisWidth}
|
||||
onEllipsis={onJsEllipsis}
|
||||
miscDeps={[copied, expanded]}
|
||||
expanded={expanded}
|
||||
miscDeps={[copied, expanded, copyLoading]}
|
||||
>
|
||||
{(node, needEllipsis) => {
|
||||
{(node, canEllipsis) => {
|
||||
let renderNode: React.ReactNode = node;
|
||||
if (node.length && needEllipsis && topAriaLabel) {
|
||||
if (node.length && canEllipsis && !expanded && topAriaLabel) {
|
||||
renderNode = (
|
||||
<span key="show-content" aria-hidden>
|
||||
{renderNode}
|
||||
@ -522,7 +495,7 @@ const Base = React.forwardRef<HTMLElement, BlockProps>((props, ref) => {
|
||||
props,
|
||||
<>
|
||||
{renderNode}
|
||||
{renderEllipsis(needEllipsis)}
|
||||
{renderEllipsis(canEllipsis)}
|
||||
</>,
|
||||
);
|
||||
|
||||
|
@ -688,6 +688,57 @@ Array [
|
||||
</div>
|
||||
</div>
|
||||
</span>,
|
||||
<div
|
||||
class="ant-typography"
|
||||
>
|
||||
Request copy text.
|
||||
<div
|
||||
aria-label="Copy"
|
||||
class="ant-typography-copy"
|
||||
role="button"
|
||||
style="border: 0px; background: transparent; padding: 0px; line-height: inherit; display: inline-block;"
|
||||
tabindex="0"
|
||||
>
|
||||
<span
|
||||
aria-label="copy"
|
||||
class="anticon anticon-copy"
|
||||
role="img"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
data-icon="copy"
|
||||
fill="currentColor"
|
||||
focusable="false"
|
||||
height="1em"
|
||||
viewBox="64 64 896 896"
|
||||
width="1em"
|
||||
>
|
||||
<path
|
||||
d="M832 64H296c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h496v688c0 4.4 3.6 8 8 8h56c4.4 0 8-3.6 8-8V96c0-17.7-14.3-32-32-32zM704 192H192c-17.7 0-32 14.3-32 32v530.7c0 8.5 3.4 16.6 9.4 22.6l173.3 173.3c2.2 2.2 4.7 4 7.4 5.5v1.9h4.2c3.5 1.3 7.2 2 11 2H704c17.7 0 32-14.3 32-32V224c0-17.7-14.3-32-32-32zM350 856.2L263.9 770H350v86.2zM664 888H414V746c0-22.1-17.9-40-40-40H232V264h432v624z"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
class="ant-tooltip ant-zoom-big-fast-appear ant-zoom-big-fast-appear-prepare ant-zoom-big-fast 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"
|
||||
role="tooltip"
|
||||
>
|
||||
Copy
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>,
|
||||
]
|
||||
`;
|
||||
|
||||
@ -1520,6 +1571,139 @@ Array [
|
||||
|
||||
exports[`renders components/typography/demo/ellipsis.tsx extend context correctly 2`] = `[]`;
|
||||
|
||||
exports[`renders components/typography/demo/ellipsis-controlled.tsx extend context correctly 1`] = `
|
||||
<div
|
||||
class="ant-flex ant-flex-align-stretch ant-flex-vertical"
|
||||
style="gap: 16px;"
|
||||
>
|
||||
<div
|
||||
class="ant-flex ant-flex-align-center"
|
||||
style="gap: 16px;"
|
||||
>
|
||||
<button
|
||||
aria-checked="false"
|
||||
class="ant-switch"
|
||||
role="switch"
|
||||
style="flex: 0 0 auto;"
|
||||
type="button"
|
||||
>
|
||||
<div
|
||||
class="ant-switch-handle"
|
||||
/>
|
||||
<span
|
||||
class="ant-switch-inner"
|
||||
>
|
||||
<span
|
||||
class="ant-switch-inner-checked"
|
||||
/>
|
||||
<span
|
||||
class="ant-switch-inner-unchecked"
|
||||
/>
|
||||
</span>
|
||||
</button>
|
||||
<div
|
||||
class="ant-slider ant-slider-horizontal"
|
||||
style="flex-basis: auto;"
|
||||
>
|
||||
<div
|
||||
class="ant-slider-rail"
|
||||
/>
|
||||
<div
|
||||
class="ant-slider-track"
|
||||
style="left: 0%; width: 5.263157894736842%;"
|
||||
/>
|
||||
<div
|
||||
class="ant-slider-step"
|
||||
/>
|
||||
<div
|
||||
aria-disabled="false"
|
||||
aria-orientation="horizontal"
|
||||
aria-valuemax="20"
|
||||
aria-valuemin="1"
|
||||
aria-valuenow="2"
|
||||
class="ant-slider-handle"
|
||||
role="slider"
|
||||
style="left: 5.263157894736842%; transform: translateX(-50%);"
|
||||
tabindex="0"
|
||||
/>
|
||||
<div
|
||||
class="ant-tooltip ant-zoom-big-fast-appear ant-zoom-big-fast-appear-prepare ant-zoom-big-fast ant-slider-tooltip 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"
|
||||
role="tooltip"
|
||||
>
|
||||
2
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
aria-label="Ant Design, a design language for background applications, is refined by Ant UED Team.Ant Design, a design language for background applications, is refined by Ant UED Team.Ant Design, a design language for background applications, is refined by Ant UED Team.Ant Design, a design language for background applications, is refined by Ant UED Team.Ant Design, a design language for background applications, is refined by Ant UED Team.Ant Design, a design language for background applications, is refined by Ant UED Team.Ant Design, a design language for background applications, is refined by Ant UED Team.Ant Design, a design language for background applications, is refined by Ant UED Team.Ant Design, a design language for background applications, is refined by Ant UED Team.Ant Design, a design language for background applications, is refined by Ant UED Team.Ant Design, a design language for background applications, is refined by Ant UED Team.Ant Design, a design language for background applications, is refined by Ant UED Team.Ant Design, a design language for background applications, is refined by Ant UED Team.Ant Design, a design language for background applications, is refined by Ant UED Team.Ant Design, a design language for background applications, is refined by Ant UED Team.Ant Design, a design language for background applications, is refined by Ant UED Team.Ant Design, a design language for background applications, is refined by Ant UED Team.Ant Design, a design language for background applications, is refined by Ant UED Team.Ant Design, a design language for background applications, is refined by Ant UED Team.Ant Design, a design language for background applications, is refined by Ant UED Team."
|
||||
class="ant-typography ant-typography-ellipsis"
|
||||
>
|
||||
Ant Design, a design language for background applications, is refined by Ant UED Team.Ant Design, a design language for background applications, is refined by Ant UED Team.Ant Design, a design language for background applications, is refined by Ant UED Team.Ant Design, a design language for background applications, is refined by Ant UED Team.Ant Design, a design language for background applications, is refined by Ant UED Team.Ant Design, a design language for background applications, is refined by Ant UED Team.Ant Design, a design language for background applications, is refined by Ant UED Team.Ant Design, a design language for background applications, is refined by Ant UED Team.Ant Design, a design language for background applications, is refined by Ant UED Team.Ant Design, a design language for background applications, is refined by Ant UED Team.Ant Design, a design language for background applications, is refined by Ant UED Team.Ant Design, a design language for background applications, is refined by Ant UED Team.Ant Design, a design language for background applications, is refined by Ant UED Team.Ant Design, a design language for background applications, is refined by Ant UED Team.Ant Design, a design language for background applications, is refined by Ant UED Team.Ant Design, a design language for background applications, is refined by Ant UED Team.Ant Design, a design language for background applications, is refined by Ant UED Team.Ant Design, a design language for background applications, is refined by Ant UED Team.Ant Design, a design language for background applications, is refined by Ant UED Team.Ant Design, a design language for background applications, is refined by Ant UED Team.
|
||||
<div
|
||||
aria-label="Copy"
|
||||
class="ant-typography-copy"
|
||||
role="button"
|
||||
style="border: 0px; background: transparent; padding: 0px; line-height: inherit; display: inline-block;"
|
||||
tabindex="0"
|
||||
>
|
||||
<span
|
||||
aria-label="copy"
|
||||
class="anticon anticon-copy"
|
||||
role="img"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
data-icon="copy"
|
||||
fill="currentColor"
|
||||
focusable="false"
|
||||
height="1em"
|
||||
viewBox="64 64 896 896"
|
||||
width="1em"
|
||||
>
|
||||
<path
|
||||
d="M832 64H296c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h496v688c0 4.4 3.6 8 8 8h56c4.4 0 8-3.6 8-8V96c0-17.7-14.3-32-32-32zM704 192H192c-17.7 0-32 14.3-32 32v530.7c0 8.5 3.4 16.6 9.4 22.6l173.3 173.3c2.2 2.2 4.7 4 7.4 5.5v1.9h4.2c3.5 1.3 7.2 2 11 2H704c17.7 0 32-14.3 32-32V224c0-17.7-14.3-32-32-32zM350 856.2L263.9 770H350v86.2zM664 888H414V746c0-22.1-17.9-40-40-40H232V264h432v624z"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
class="ant-tooltip ant-zoom-big-fast-appear ant-zoom-big-fast-appear-prepare ant-zoom-big-fast 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"
|
||||
role="tooltip"
|
||||
>
|
||||
Copy
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`renders components/typography/demo/ellipsis-controlled.tsx extend context correctly 2`] = `[]`;
|
||||
|
||||
exports[`renders components/typography/demo/ellipsis-debug.tsx extend context correctly 1`] = `
|
||||
Array [
|
||||
<button
|
||||
|
@ -593,6 +593,38 @@ Array [
|
||||
</span>
|
||||
</div>
|
||||
</span>,
|
||||
<div
|
||||
class="ant-typography"
|
||||
>
|
||||
Request copy text.
|
||||
<div
|
||||
aria-label="Copy"
|
||||
class="ant-typography-copy"
|
||||
role="button"
|
||||
style="border:0;background:transparent;padding:0;line-height:inherit;display:inline-block"
|
||||
tabindex="0"
|
||||
>
|
||||
<span
|
||||
aria-label="copy"
|
||||
class="anticon anticon-copy"
|
||||
role="img"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
data-icon="copy"
|
||||
fill="currentColor"
|
||||
focusable="false"
|
||||
height="1em"
|
||||
viewBox="64 64 896 896"
|
||||
width="1em"
|
||||
>
|
||||
<path
|
||||
d="M832 64H296c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h496v688c0 4.4 3.6 8 8 8h56c4.4 0 8-3.6 8-8V96c0-17.7-14.3-32-32-32zM704 192H192c-17.7 0-32 14.3-32 32v530.7c0 8.5 3.4 16.6 9.4 22.6l173.3 173.3c2.2 2.2 4.7 4 7.4 5.5v1.9h4.2c3.5 1.3 7.2 2 11 2H704c17.7 0 32-14.3 32-32V224c0-17.7-14.3-32-32-32zM350 856.2L263.9 770H350v86.2zM664 888H414V746c0-22.1-17.9-40-40-40H232V264h432v624z"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
</div>
|
||||
</div>,
|
||||
]
|
||||
`;
|
||||
|
||||
@ -1143,6 +1175,99 @@ Array [
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`renders components/typography/demo/ellipsis-controlled.tsx correctly 1`] = `
|
||||
<div
|
||||
class="ant-flex ant-flex-align-stretch ant-flex-vertical"
|
||||
style="gap:16px"
|
||||
>
|
||||
<div
|
||||
class="ant-flex ant-flex-align-center"
|
||||
style="gap:16px"
|
||||
>
|
||||
<button
|
||||
aria-checked="false"
|
||||
class="ant-switch"
|
||||
role="switch"
|
||||
style="flex:none"
|
||||
type="button"
|
||||
>
|
||||
<div
|
||||
class="ant-switch-handle"
|
||||
/>
|
||||
<span
|
||||
class="ant-switch-inner"
|
||||
>
|
||||
<span
|
||||
class="ant-switch-inner-checked"
|
||||
/>
|
||||
<span
|
||||
class="ant-switch-inner-unchecked"
|
||||
/>
|
||||
</span>
|
||||
</button>
|
||||
<div
|
||||
class="ant-slider ant-slider-horizontal"
|
||||
style="flex:auto"
|
||||
>
|
||||
<div
|
||||
class="ant-slider-rail"
|
||||
/>
|
||||
<div
|
||||
class="ant-slider-track"
|
||||
style="left:0%;width:5.263157894736842%"
|
||||
/>
|
||||
<div
|
||||
class="ant-slider-step"
|
||||
/>
|
||||
<div
|
||||
aria-disabled="false"
|
||||
aria-orientation="horizontal"
|
||||
aria-valuemax="20"
|
||||
aria-valuemin="1"
|
||||
aria-valuenow="2"
|
||||
class="ant-slider-handle"
|
||||
role="slider"
|
||||
style="left:5.263157894736842%;transform:translateX(-50%)"
|
||||
tabindex="0"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
aria-label="Ant Design, a design language for background applications, is refined by Ant UED Team.Ant Design, a design language for background applications, is refined by Ant UED Team.Ant Design, a design language for background applications, is refined by Ant UED Team.Ant Design, a design language for background applications, is refined by Ant UED Team.Ant Design, a design language for background applications, is refined by Ant UED Team.Ant Design, a design language for background applications, is refined by Ant UED Team.Ant Design, a design language for background applications, is refined by Ant UED Team.Ant Design, a design language for background applications, is refined by Ant UED Team.Ant Design, a design language for background applications, is refined by Ant UED Team.Ant Design, a design language for background applications, is refined by Ant UED Team.Ant Design, a design language for background applications, is refined by Ant UED Team.Ant Design, a design language for background applications, is refined by Ant UED Team.Ant Design, a design language for background applications, is refined by Ant UED Team.Ant Design, a design language for background applications, is refined by Ant UED Team.Ant Design, a design language for background applications, is refined by Ant UED Team.Ant Design, a design language for background applications, is refined by Ant UED Team.Ant Design, a design language for background applications, is refined by Ant UED Team.Ant Design, a design language for background applications, is refined by Ant UED Team.Ant Design, a design language for background applications, is refined by Ant UED Team.Ant Design, a design language for background applications, is refined by Ant UED Team."
|
||||
class="ant-typography ant-typography-ellipsis"
|
||||
>
|
||||
Ant Design, a design language for background applications, is refined by Ant UED Team.Ant Design, a design language for background applications, is refined by Ant UED Team.Ant Design, a design language for background applications, is refined by Ant UED Team.Ant Design, a design language for background applications, is refined by Ant UED Team.Ant Design, a design language for background applications, is refined by Ant UED Team.Ant Design, a design language for background applications, is refined by Ant UED Team.Ant Design, a design language for background applications, is refined by Ant UED Team.Ant Design, a design language for background applications, is refined by Ant UED Team.Ant Design, a design language for background applications, is refined by Ant UED Team.Ant Design, a design language for background applications, is refined by Ant UED Team.Ant Design, a design language for background applications, is refined by Ant UED Team.Ant Design, a design language for background applications, is refined by Ant UED Team.Ant Design, a design language for background applications, is refined by Ant UED Team.Ant Design, a design language for background applications, is refined by Ant UED Team.Ant Design, a design language for background applications, is refined by Ant UED Team.Ant Design, a design language for background applications, is refined by Ant UED Team.Ant Design, a design language for background applications, is refined by Ant UED Team.Ant Design, a design language for background applications, is refined by Ant UED Team.Ant Design, a design language for background applications, is refined by Ant UED Team.Ant Design, a design language for background applications, is refined by Ant UED Team.
|
||||
<div
|
||||
aria-label="Copy"
|
||||
class="ant-typography-copy"
|
||||
role="button"
|
||||
style="border:0;background:transparent;padding:0;line-height:inherit;display:inline-block"
|
||||
tabindex="0"
|
||||
>
|
||||
<span
|
||||
aria-label="copy"
|
||||
class="anticon anticon-copy"
|
||||
role="img"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
data-icon="copy"
|
||||
fill="currentColor"
|
||||
focusable="false"
|
||||
height="1em"
|
||||
viewBox="64 64 896 896"
|
||||
width="1em"
|
||||
>
|
||||
<path
|
||||
d="M832 64H296c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h496v688c0 4.4 3.6 8 8 8h56c4.4 0 8-3.6 8-8V96c0-17.7-14.3-32-32-32zM704 192H192c-17.7 0-32 14.3-32 32v530.7c0 8.5 3.4 16.6 9.4 22.6l173.3 173.3c2.2 2.2 4.7 4 7.4 5.5v1.9h4.2c3.5 1.3 7.2 2 11 2H704c17.7 0 32-14.3 32-32V224c0-17.7-14.3-32-32-32zM350 856.2L263.9 770H350v86.2zM664 888H414V746c0-22.1-17.9-40-40-40H232V264h432v624z"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`renders components/typography/demo/ellipsis-debug.tsx correctly 1`] = `
|
||||
Array [
|
||||
<button
|
||||
|
@ -2,8 +2,9 @@ import React from 'react';
|
||||
import { LikeOutlined, SmileOutlined } from '@ant-design/icons';
|
||||
import * as copyObj from 'copy-to-clipboard';
|
||||
|
||||
import { fireEvent, render, waitFakeTimer, waitFor } from '../../../tests/utils';
|
||||
import { fireEvent, render, renderHook, waitFakeTimer, waitFor } from '../../../tests/utils';
|
||||
import Base from '../Base';
|
||||
import useCopyClick from '../hooks/useCopyClick';
|
||||
|
||||
describe('Typography copy', () => {
|
||||
const errorSpy = jest.spyOn(console, 'error').mockImplementation(() => {});
|
||||
@ -262,6 +263,39 @@ describe('Typography copy', () => {
|
||||
fireEvent.click(copyBtn);
|
||||
expect(spy.mock.calls[0][0]).toEqual(nextText);
|
||||
jest.useRealTimers();
|
||||
spy.mockReset();
|
||||
});
|
||||
|
||||
it('copy by async', async () => {
|
||||
const spy = jest.spyOn(copyObj, 'default');
|
||||
const { container: wrapper } = render(
|
||||
<Base
|
||||
component="p"
|
||||
copyable={{
|
||||
text: jest.fn().mockResolvedValueOnce('Request text'),
|
||||
}}
|
||||
>
|
||||
test copy
|
||||
</Base>,
|
||||
);
|
||||
fireEvent.click(wrapper.querySelectorAll('.ant-typography-copy')[0]);
|
||||
expect(wrapper.querySelectorAll('.anticon-loading')[0]).toBeTruthy();
|
||||
await waitFakeTimer();
|
||||
expect(spy.mock.calls[0][0]).toEqual('Request text');
|
||||
spy.mockReset();
|
||||
expect(wrapper.querySelectorAll('.anticon-loading')[0]).toBeFalsy();
|
||||
});
|
||||
|
||||
it('useCopyClick error', async () => {
|
||||
const { result } = renderHook(() =>
|
||||
useCopyClick({
|
||||
copyConfig: {
|
||||
text: jest.fn().mockRejectedValueOnce('Oops'),
|
||||
},
|
||||
}),
|
||||
);
|
||||
await expect(() => result.current?.onClick?.()).rejects.toMatch('Oops');
|
||||
expect(result.current?.copyLoading).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -249,6 +249,34 @@ describe('Typography.Ellipsis', () => {
|
||||
expect(container.querySelector('p')?.textContent).toEqual(fullStr);
|
||||
});
|
||||
|
||||
it('should collapsible work', async () => {
|
||||
const ref = React.createRef<HTMLElement>();
|
||||
|
||||
const { container: wrapper } = render(
|
||||
<Base
|
||||
ellipsis={{
|
||||
expandable: 'collapsible',
|
||||
symbol: (expanded) => (expanded ? 'CloseIt' : 'OpenIt'),
|
||||
}}
|
||||
component="p"
|
||||
ref={ref}
|
||||
>
|
||||
{fullStr}
|
||||
</Base>,
|
||||
);
|
||||
|
||||
triggerResize(ref.current!);
|
||||
await waitFakeTimer();
|
||||
|
||||
expect(wrapper.querySelector('p')?.textContent).toEqual(`Bamboo is L...OpenIt`);
|
||||
|
||||
fireEvent.click(wrapper.querySelector('.ant-typography-expand')!);
|
||||
expect(wrapper.querySelector('p')?.textContent).toEqual(`${fullStr}CloseIt`);
|
||||
|
||||
fireEvent.click(wrapper.querySelector('.ant-typography-collapse')!);
|
||||
expect(wrapper.querySelector('p')?.textContent).toEqual(`Bamboo is L...OpenIt`);
|
||||
});
|
||||
|
||||
it('should have custom expand style', async () => {
|
||||
const ref = React.createRef<HTMLElement>();
|
||||
const symbol = 'more';
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user