Compare commits

...

22 Commits

Author SHA1 Message Date
lijianan 5bfc7297d0
Merge branch 'feature' into affix-r 1 year ago
github-actions[bot] b0d69a6878
chore: auto merge branches (#43575) 1 year ago
红果汁 c377457104
feat: ColorPicker implement `disabledAlpha` API (#43355) 1 year ago
github-actions[bot] 493b66b641
chore: auto merge branches (#43498) 1 year ago
二货爱吃白萝卜 b072d3a02c
feat: Modal hooks confirm function support await (#43470) 1 year ago
lijianan d0f32be454
Merge branch 'feature' into affix-r 1 year ago
lijianan 6628414a6f
Merge branch 'feature' into affix-r 1 year ago
lijianan 1f814f64fa
Merge branch 'feature' into affix-r 1 year ago
栗嘉男 72809636b9 fix 1 year ago
栗嘉男 2ae3305a2e fix 1 year ago
lijianan db8a012529
Merge branch 'feature' into affix-r 1 year ago
栗嘉男 6daede69b1 Merge branch feature into affix-r 2 years ago
栗嘉男 31dc0d3250 fix 2 years ago
栗嘉男 ec38148e0d Merge branch 'affix-r' of https://github.com/ant-design/ant-design into affix-r 2 years ago
lijianan a7d5505bcd
Update index.ts 2 years ago
栗嘉男 8826f0346d test 2 years ago
lijianan ca466c7437
Merge branch 'feature' into affix-r 2 years ago
lijianan 28e3b5ca95
Merge branch 'master' into affix-r 2 years ago
lijianan 9ee099a094
Merge branch 'master' into affix-r 2 years ago
栗嘉男 5a4959b716 Merge branch master into affix-r 2 years ago
栗嘉男 c093f2f388 Merge branch 'master' into affix-r 2 years ago
栗嘉男 4167e62e6b fix 2 years ago
  1. 12
      components/_util/ActionButton.tsx
  2. 116
      components/affix/__tests__/Affix.test.tsx
  3. 373
      components/affix/index.tsx
  4. 38
      components/color-picker/ColorPicker.tsx
  5. 285
      components/color-picker/__tests__/__snapshots__/demo-extend.test.ts.snap
  6. 15
      components/color-picker/__tests__/__snapshots__/demo.test.ts.snap
  7. 53
      components/color-picker/__tests__/index.test.tsx
  8. 8
      components/color-picker/components/ColorInput.tsx
  9. 12
      components/color-picker/components/PanelPicker.tsx
  10. 7
      components/color-picker/demo/disabled-alpha.md
  11. 6
      components/color-picker/demo/disabled-alpha.tsx
  12. 2
      components/color-picker/index.en-US.md
  13. 2
      components/color-picker/index.zh-CN.md
  14. 1
      components/color-picker/interface.ts
  15. 11
      components/color-picker/style/picker.ts
  16. 6
      components/color-picker/util.ts
  17. 25
      components/modal/ConfirmDialog.tsx
  18. 44
      components/modal/__tests__/hook.test.tsx
  19. 4
      components/modal/demo/hooks.md
  20. 9
      components/modal/demo/hooks.tsx
  21. 15
      components/modal/index.en-US.md
  22. 15
      components/modal/index.zh-CN.md
  23. 8
      components/modal/useModal/HookModal.tsx
  24. 35
      components/modal/useModal/index.tsx

12
components/_util/ActionButton.tsx

@ -14,6 +14,11 @@ export interface ActionButtonProps {
emitEvent?: boolean;
quitOnNullishReturnValue?: boolean;
children?: React.ReactNode;
/**
* Do not throw if is await mode
*/
isSilent?: () => boolean;
}
function isThenable<T extends any>(thing?: PromiseLike<T>): boolean {
@ -29,6 +34,7 @@ const ActionButton: React.FC<ActionButtonProps> = (props) => {
close,
autoFocus,
emitEvent,
isSilent,
quitOnNullishReturnValue,
actionFn,
} = props;
@ -70,6 +76,12 @@ const ActionButton: React.FC<ActionButtonProps> = (props) => {
// See: https://github.com/ant-design/ant-design/issues/6183
setLoading(false, true);
clickedRef.current = false;
// Do not throw if is `await` mode
if (isSilent?.()) {
return;
}
return Promise.reject(e);
},
);

116
components/affix/__tests__/Affix.test.tsx

@ -1,10 +1,9 @@
import type { CSSProperties } from 'react';
import React, { useEffect, useRef } from 'react';
import type { InternalAffixClass } from '..';
import Affix from '..';
import accessibilityTest from '../../../tests/shared/accessibilityTest';
import rtlTest from '../../../tests/shared/rtlTest';
import { render, triggerResize, waitFakeTimer } from '../../../tests/utils';
import { render, waitFakeTimer } from '../../../tests/utils';
import Button from '../../button';
const events: Partial<Record<keyof HTMLElementEventMap, (ev: Partial<Event>) => void>> = {};
@ -14,11 +13,9 @@ interface AffixProps {
offsetBottom?: number;
style?: CSSProperties;
onChange?: () => void;
onTestUpdatePosition?: () => void;
getInstance?: (inst: InternalAffixClass) => void;
}
const AffixMounter: React.FC<AffixProps> = ({ getInstance, ...restProps }) => {
const AffixMounter: React.FC<AffixProps> = (props) => {
const container = useRef<HTMLDivElement>(null);
useEffect(() => {
if (container.current) {
@ -31,8 +28,8 @@ const AffixMounter: React.FC<AffixProps> = ({ getInstance, ...restProps }) => {
}, []);
return (
<div ref={container} className="container">
<Affix className="fixed" ref={getInstance} target={() => container.current} {...restProps}>
<Button type="primary">Fixed at the top of container</Button>
<Affix className="fixed" target={() => container.current} {...props}>
<Button>Fixed at the top of container</Button>
</Affix>
</div>
);
@ -124,36 +121,6 @@ describe('Affix Render', () => {
});
describe('updatePosition when target changed', () => {
it('function change', async () => {
document.body.innerHTML = '<div id="mounter" />';
const container = document.getElementById('mounter');
const getTarget = () => container;
let affixInstance: InternalAffixClass;
const { rerender } = render(
<Affix
ref={(node) => {
affixInstance = node as InternalAffixClass;
}}
target={getTarget}
>
{null}
</Affix>,
);
rerender(
<Affix
ref={(node) => {
affixInstance = node as InternalAffixClass;
}}
target={() => null}
>
{null}
</Affix>,
);
expect(affixInstance!.state.status).toBe(0);
expect(affixInstance!.state.affixStyle).toBe(undefined);
expect(affixInstance!.state.placeholderStyle).toBe(undefined);
});
it('check position change before measure', async () => {
const { container } = render(
<>
@ -169,80 +136,5 @@ describe('Affix Render', () => {
await movePlaceholder(1000);
expect(container.querySelector('.ant-affix')).toBeTruthy();
});
it('do not measure when hidden', async () => {
let affixInstance: InternalAffixClass | null = null;
const { rerender } = render(
<AffixMounter
getInstance={(inst) => {
affixInstance = inst;
}}
offsetBottom={0}
/>,
);
await waitFakeTimer();
const firstAffixStyle = affixInstance!.state.affixStyle;
rerender(
<AffixMounter
getInstance={(inst) => {
affixInstance = inst;
}}
offsetBottom={0}
style={{ display: 'none' }}
/>,
);
await waitFakeTimer();
const secondAffixStyle = affixInstance!.state.affixStyle;
expect(firstAffixStyle).toEqual(secondAffixStyle);
});
});
describe('updatePosition when size changed', () => {
it('add class automatically', async () => {
document.body.innerHTML = '<div id="mounter" />';
let affixInstance: InternalAffixClass | null = null;
render(
<AffixMounter
getInstance={(inst) => {
affixInstance = inst;
}}
offsetBottom={0}
/>,
{
container: document.getElementById('mounter')!,
},
);
await waitFakeTimer();
await movePlaceholder(300);
expect(affixInstance!.state.affixStyle).toBeTruthy();
});
// Trigger inner and outer element for the two <ResizeObserver>s.
[
'.ant-btn', // inner
'.fixed', // outer
].forEach((selector) => {
it(`trigger listener when size change: ${selector}`, async () => {
const updateCalled = jest.fn();
const { container } = render(
<AffixMounter offsetBottom={0} onTestUpdatePosition={updateCalled} />,
{
container: document.getElementById('mounter')!,
},
);
updateCalled.mockReset();
triggerResize(container.querySelector(selector)!);
await waitFakeTimer();
expect(updateCalled).toHaveBeenCalled();
});
});
});
});

373
components/affix/index.tsx

@ -3,7 +3,7 @@
import classNames from 'classnames';
import ResizeObserver from 'rc-resize-observer';
import omit from 'rc-util/lib/omit';
import React, { createRef, forwardRef, useContext } from 'react';
import React, { useContext, useEffect, useMemo, useRef, useState } from 'react';
import throttleByAnimationFrame from '../_util/throttleByAnimationFrame';
import type { ConfigConsumerProps } from '../config-provider';
import { ConfigContext } from '../config-provider';
@ -41,279 +41,178 @@ export interface AffixProps {
children: React.ReactNode;
}
interface InternalAffixProps extends AffixProps {
affixPrefixCls: string;
}
enum AffixStatus {
None,
Prepare,
}
export interface AffixState {
affixStyle?: React.CSSProperties;
placeholderStyle?: React.CSSProperties;
status: AffixStatus;
lastAffix: boolean;
prevTarget: Window | HTMLElement | null;
interface AffixRef {
updatePosition: ReturnType<typeof throttleByAnimationFrame>;
}
class InternalAffix extends React.Component<InternalAffixProps, AffixState> {
static contextType = ConfigContext;
state: AffixState = {
status: AffixStatus.None,
lastAffix: false,
prevTarget: null,
};
private placeholderNodeRef = createRef<HTMLDivElement>();
const Affix = React.forwardRef<AffixRef, AffixProps>((props, ref) => {
const {
rootClassName: customizeRootClassName,
children,
offsetBottom,
offsetTop,
prefixCls: customizePrefixCls,
onChange,
target: customizeTarget,
} = props;
const [lastAffix, setLastAffix] = useState<boolean>(false);
const [status, setStatus] = useState<AffixStatus>(AffixStatus.None);
const [affixStyle, setAffixStyle] = useState<React.CSSProperties>();
const [placeholderStyle, setPlaceholderStyle] = useState<React.CSSProperties>();
const placeholderNodeRef = useRef<HTMLDivElement>(null);
const fixedNodeRef = useRef<HTMLDivElement>(null);
const timerRef = useRef<NodeJS.Timeout | null>(null);
const { getTargetContainer, getPrefixCls } = useContext<ConfigConsumerProps>(ConfigContext);
private fixedNodeRef = createRef<HTMLDivElement>();
private timer: NodeJS.Timeout | null;
context: ConfigConsumerProps;
private getTargetFunc() {
const { getTargetContainer } = this.context;
const { target } = this.props;
if (target !== undefined) {
return target;
}
const affixPrefixCls = getPrefixCls('affix', customizePrefixCls);
return getTargetContainer ?? getDefaultTarget;
}
const target = useMemo<ReturnType<NonNullable<AffixProps['target']>>>(
() => (customizeTarget ?? getTargetContainer ?? getDefaultTarget)(),
[customizeTarget, getTargetContainer],
);
addListeners = () => {
const targetFunc = this.getTargetFunc();
const target = targetFunc?.();
const { prevTarget } = this.state;
if (prevTarget !== target) {
TRIGGER_EVENTS.forEach((eventName) => {
prevTarget?.removeEventListener(eventName, this.lazyUpdatePosition);
target?.addEventListener(eventName, this.lazyUpdatePosition);
});
this.updatePosition();
this.setState({ prevTarget: target });
}
};
removeListeners = () => {
if (this.timer) {
clearTimeout(this.timer);
this.timer = null;
}
const { prevTarget } = this.state;
const targetFunc = this.getTargetFunc();
const newTarget = targetFunc?.();
TRIGGER_EVENTS.forEach((eventName) => {
newTarget?.removeEventListener(eventName, this.lazyUpdatePosition);
prevTarget?.removeEventListener(eventName, this.lazyUpdatePosition);
});
this.updatePosition.cancel();
// https://github.com/ant-design/ant-design/issues/22683
this.lazyUpdatePosition.cancel();
};
const memoOffsetTop = useMemo<number>(
() => (offsetBottom === undefined && offsetTop === undefined ? 0 : (offsetTop as number)),
[offsetBottom, offsetTop],
);
// Event handler
componentDidMount() {
// [Legacy] Wait for parent component ref has its value.
// We should use target as directly element instead of function which makes element check hard.
this.timer = setTimeout(this.addListeners);
}
componentDidUpdate(prevProps: AffixProps) {
this.addListeners();
const measure = () => {
if (
prevProps.offsetTop !== this.props.offsetTop ||
prevProps.offsetBottom !== this.props.offsetBottom
status === AffixStatus.Prepare ||
!target ||
!fixedNodeRef.current ||
!placeholderNodeRef.current
) {
this.updatePosition();
return;
}
this.measure();
}
componentWillUnmount() {
this.removeListeners();
}
getOffsetTop = () => {
const { offsetBottom, offsetTop } = this.props;
return offsetBottom === undefined && offsetTop === undefined ? 0 : offsetTop;
};
getOffsetBottom = () => this.props.offsetBottom;
// =================== Measure ===================
measure = () => {
const { status, lastAffix } = this.state;
const { onChange } = this.props;
const targetFunc = this.getTargetFunc();
const placeholderRect = getTargetRect(placeholderNodeRef.current);
if (
status !== AffixStatus.Prepare ||
!this.fixedNodeRef.current ||
!this.placeholderNodeRef.current ||
!targetFunc
placeholderRect.top === 0 &&
placeholderRect.left === 0 &&
placeholderRect.width === 0 &&
placeholderRect.height === 0
) {
return;
}
const offsetTop = this.getOffsetTop();
const offsetBottom = this.getOffsetBottom();
const targetNode = targetFunc();
if (targetNode) {
const newState: Partial<AffixState> = {
status: AffixStatus.None,
};
const placeholderRect = getTargetRect(this.placeholderNodeRef.current);
if (
placeholderRect.top === 0 &&
placeholderRect.left === 0 &&
placeholderRect.width === 0 &&
placeholderRect.height === 0
) {
return;
}
const targetRect = getTargetRect(targetNode);
const fixedTop = getFixedTop(placeholderRect, targetRect, offsetTop);
const fixedBottom = getFixedBottom(placeholderRect, targetRect, offsetBottom);
if (fixedTop !== undefined) {
newState.affixStyle = {
position: 'fixed',
top: fixedTop,
width: placeholderRect.width,
height: placeholderRect.height,
};
newState.placeholderStyle = {
width: placeholderRect.width,
height: placeholderRect.height,
};
} else if (fixedBottom !== undefined) {
newState.affixStyle = {
position: 'fixed',
bottom: fixedBottom,
width: placeholderRect.width,
height: placeholderRect.height,
};
newState.placeholderStyle = {
width: placeholderRect.width,
height: placeholderRect.height,
};
}
const targetRect = getTargetRect(target);
const fixedTop = getFixedTop(placeholderRect, targetRect, memoOffsetTop);
const fixedBottom = getFixedBottom(placeholderRect, targetRect, offsetBottom);
newState.lastAffix = !!newState.affixStyle;
if (onChange && lastAffix !== newState.lastAffix) {
onChange(newState.lastAffix);
}
this.setState(newState as AffixState);
if (fixedTop !== undefined) {
setAffixStyle({
position: 'fixed',
top: fixedTop,
width: placeholderRect.width,
height: placeholderRect.height,
});
setPlaceholderStyle({
width: placeholderRect.width,
height: placeholderRect.height,
});
} else if (fixedBottom !== undefined) {
setAffixStyle({
position: 'fixed',
bottom: fixedBottom,
width: placeholderRect.width,
height: placeholderRect.height,
});
setPlaceholderStyle({
width: placeholderRect.width,
height: placeholderRect.height,
});
}
setLastAffix((prevState) => {
if (lastAffix !== prevState) {
onChange?.(!!affixStyle);
return !!affixStyle;
}
return lastAffix;
});
};
prepareMeasure = () => {
const prepareMeasure = () => {
// event param is used before. Keep compatible ts define here.
this.setState({
status: AffixStatus.Prepare,
affixStyle: undefined,
placeholderStyle: undefined,
});
// Test if `updatePosition` called
if (process.env.NODE_ENV === 'test') {
const { onTestUpdatePosition } = this.props as any;
onTestUpdatePosition?.();
}
setStatus(AffixStatus.Prepare);
setAffixStyle(undefined);
setPlaceholderStyle(undefined);
};
updatePosition = throttleByAnimationFrame(() => {
this.prepareMeasure();
});
lazyUpdatePosition = throttleByAnimationFrame(() => {
const targetFunc = this.getTargetFunc();
const { affixStyle } = this.state;
const updatePosition = throttleByAnimationFrame(prepareMeasure);
const lazyUpdatePosition = throttleByAnimationFrame(() => {
// Check position change before measure to make Safari smooth
if (targetFunc && affixStyle) {
const offsetTop = this.getOffsetTop();
const offsetBottom = this.getOffsetBottom();
const targetNode = targetFunc();
if (targetNode && this.placeholderNodeRef.current) {
const targetRect = getTargetRect(targetNode);
const placeholderRect = getTargetRect(this.placeholderNodeRef.current);
const fixedTop = getFixedTop(placeholderRect, targetRect, offsetTop);
const fixedBottom = getFixedBottom(placeholderRect, targetRect, offsetBottom);
if (
(fixedTop !== undefined && affixStyle.top === fixedTop) ||
(fixedBottom !== undefined && affixStyle.bottom === fixedBottom)
) {
return;
}
if (target && placeholderNodeRef.current && affixStyle) {
const targetRect = getTargetRect(target);
const placeholderRect = getTargetRect(placeholderNodeRef.current);
const fixedTop = getFixedTop(placeholderRect, targetRect, offsetTop);
const fixedBottom = getFixedBottom(placeholderRect, targetRect, offsetBottom);
if (
(fixedTop !== undefined && affixStyle.top === fixedTop) ||
(fixedBottom !== undefined && affixStyle.bottom === fixedBottom)
) {
return;
}
}
// Directly call prepare measure since it's already throttled.
this.prepareMeasure();
prepareMeasure();
});
// =================== Render ===================
render() {
const { affixStyle, placeholderStyle } = this.state;
const { affixPrefixCls, rootClassName, children } = this.props;
const className = classNames(affixStyle && rootClassName, {
[affixPrefixCls]: !!affixStyle,
});
let props = omit(this.props, [
'prefixCls',
'offsetTop',
'offsetBottom',
'target',
'onChange',
'affixPrefixCls',
'rootClassName',
]);
// Omit this since `onTestUpdatePosition` only works on test.
if (process.env.NODE_ENV === 'test') {
props = omit(props as typeof props & { onTestUpdatePosition: any }, ['onTestUpdatePosition']);
}
React.useImperativeHandle(ref, () => ({ updatePosition }));
return (
<ResizeObserver onResize={this.updatePosition}>
<div {...props} ref={this.placeholderNodeRef}>
{affixStyle && <div style={placeholderStyle} aria-hidden="true" />}
<div className={className} ref={this.fixedNodeRef} style={affixStyle}>
<ResizeObserver onResize={this.updatePosition}>{children}</ResizeObserver>
</div>
</div>
</ResizeObserver>
);
}
}
// just use in test
export type InternalAffixClass = InternalAffix;
const Affix = forwardRef<InternalAffix, AffixProps>((props, ref) => {
const { prefixCls: customizePrefixCls, rootClassName } = props;
const { getPrefixCls } = useContext<ConfigConsumerProps>(ConfigContext);
const affixPrefixCls = getPrefixCls('affix', customizePrefixCls);
useEffect(() => {
timerRef.current = setTimeout(() => {
TRIGGER_EVENTS.forEach((eventName) => {
target?.addEventListener(eventName, lazyUpdatePosition);
});
// updatePosition();
measure();
});
return () => {
if (timerRef.current) {
clearTimeout(timerRef.current);
timerRef.current = null;
}
TRIGGER_EVENTS.forEach((eventName) => {
target?.removeEventListener(eventName, lazyUpdatePosition);
});
updatePosition.cancel();
lazyUpdatePosition.cancel();
};
}, [offsetTop, offsetBottom, customizeTarget, getTargetContainer]);
const [wrapSSR, hashId] = useStyle(affixPrefixCls);
const rootClassName = classNames(customizeRootClassName, hashId);
const AffixProps: InternalAffixProps = {
...props,
affixPrefixCls,
rootClassName: classNames(rootClassName, hashId),
};
const className = classNames(affixStyle && rootClassName, {
[affixPrefixCls]: !!affixStyle,
});
return wrapSSR(<InternalAffix {...AffixProps} ref={ref} />);
const divProps = omit(props, [
'prefixCls',
'offsetTop',
'offsetBottom',
'target',
'onChange',
'rootClassName',
]);
return wrapSSR(
<ResizeObserver onResize={updatePosition}>
<div {...divProps} ref={placeholderNodeRef}>
{affixStyle && <div style={placeholderStyle} aria-hidden="true" />}
<div className={className} ref={fixedNodeRef} style={affixStyle}>
<ResizeObserver onResize={updatePosition}>{children}</ResizeObserver>
</div>
</div>
</ResizeObserver>,
);
});
if (process.env.NODE_ENV !== 'production') {

38
components/color-picker/ColorPicker.tsx

@ -5,9 +5,10 @@ import type {
import classNames from 'classnames';
import useMergedState from 'rc-util/lib/hooks/useMergedState';
import type { CSSProperties, FC } from 'react';
import React, { useContext, useRef, useState } from 'react';
import React, { useContext, useMemo, useRef, useState } from 'react';
import genPurePanel from '../_util/PurePanel';
import { getStatusClassNames } from '../_util/statusUtils';
import warning from '../_util/warning';
import type { SizeType } from '../config-provider/SizeContext';
import type { ConfigConsumerProps } from '../config-provider/context';
import { ConfigContext } from '../config-provider/context';
@ -29,11 +30,11 @@ import type {
TriggerType,
} from './interface';
import useStyle from './style/index';
import { customizePrefixCls, generateColor } from './util';
import { customizePrefixCls, genAlphaColor, generateColor, getAlphaColor } from './util';
export type ColorPickerProps = Omit<
RcColorPickerProps,
'onChange' | 'value' | 'defaultValue' | 'panelRender' | 'onChangeComplete'
'onChange' | 'value' | 'defaultValue' | 'panelRender' | 'disabledAlpha' | 'onChangeComplete'
> & {
value?: ColorValueType;
defaultValue?: ColorValueType;
@ -54,6 +55,7 @@ export type ColorPickerProps = Omit<
size?: SizeType;
styles?: { popup?: CSSProperties; popupOverlayInner?: CSSProperties };
rootClassName?: string;
disabledAlpha?: boolean;
onOpenChange?: (open: boolean) => void;
onFormatChange?: (format: ColorFormat) => void;
onChange?: (value: Color, hex: string) => void;
@ -85,6 +87,7 @@ const ColorPicker: CompoundedComponent = (props) => {
size: customizeSize,
rootClassName,
styles,
disabledAlpha = false,
onFormatChange,
onChange,
onClear,
@ -117,6 +120,8 @@ const ColorPicker: CompoundedComponent = (props) => {
const prefixCls = getPrefixCls('color-picker', customizePrefixCls);
const isAlphaColor = useMemo(() => getAlphaColor(colorValue) < 100, [colorValue]);
// ===================== Form Status =====================
const { status: contextStatus } = React.useContext(FormItemInputContext);
@ -140,18 +145,29 @@ const ColorPicker: CompoundedComponent = (props) => {
const popupAllowCloseRef = useRef(true);
// ===================== Warning ======================
if (process.env.NODE_ENV !== 'production') {
warning(
!(disabledAlpha && isAlphaColor),
'ColorPicker',
'`disabledAlpha` will make the alpha to be 100% when use alpha color.',
);
}
const handleChange = (data: Color, type?: HsbaColorType, pickColor?: boolean) => {
let color: Color = generateColor(data);
const isNull = value === null || (!value && defaultValue === null);
if (colorCleared || isNull) {
setColorCleared(false);
const hsba = color.toHsb();
// ignore alpha slider
if (colorValue.toHsb().a === 0 && type !== 'alpha') {
hsba.a = 1;
color = generateColor(hsba);
if (getAlphaColor(colorValue) === 0 && type !== 'alpha') {
color = genAlphaColor(color);
}
}
// ignore alpha color
if (disabledAlpha && isAlphaColor) {
color = genAlphaColor(color);
}
// Only for drag-and-drop color picking
if (pickColor) {
popupAllowCloseRef.current = false;
@ -168,7 +184,12 @@ const ColorPicker: CompoundedComponent = (props) => {
const handleChangeComplete: ColorPickerProps['onChangeComplete'] = (color) => {
popupAllowCloseRef.current = true;
onChangeComplete?.(generateColor(color));
let changeColor = generateColor(color);
// ignore alpha color
if (disabledAlpha && isAlphaColor) {
changeColor = genAlphaColor(color);
}
onChangeComplete?.(changeColor);
};
const popoverProps: PopoverProps = {
@ -188,6 +209,7 @@ const ColorPicker: CompoundedComponent = (props) => {
allowClear,
colorCleared,
disabled,
disabledAlpha,
presets,
panelRender,
format: formatValue,

285
components/color-picker/__tests__/__snapshots__/demo-extend.test.ts.snap

@ -1920,6 +1920,291 @@ Array [
]
`;
exports[`renders components/color-picker/demo/disabled-alpha.tsx extend context correctly 1`] = `
Array [
<div
class="ant-color-picker-trigger"
>
<div
class="ant-color-picker-color-block"
>
<div
class="ant-color-picker-color-block-inner"
style="background: rgb(22, 119, 255);"
/>
</div>
</div>,
<div
class="ant-popover ant-zoom-big-appear ant-zoom-big-appear-prepare ant-zoom-big ant-color-picker ant-popover-placement-bottomLeft"
style="--arrow-x: 0px; --arrow-y: 0px; left: -1000vw; top: -1000vh; box-sizing: border-box;"
>
<div
class="ant-popover-arrow"
style="position: absolute;"
/>
<div
class="ant-popover-content"
>
<div
class="ant-popover-inner"
role="tooltip"
>
<div
class="ant-popover-inner-content"
>
<div
class="ant-color-picker-inner-content"
>
<div
class="ant-color-picker-panel"
>
<div
class="ant-color-picker-select"
>
<div
class="ant-color-picker-palette"
style="position: relative;"
>
<div
style="position: absolute; left: 0px; top: 0px; z-index: 1;"
>
<div
class="ant-color-picker-handler"
style="background-color: rgb(22, 119, 255);"
/>
</div>
<div
class="ant-color-picker-saturation"
style="background-color: rgb(0, 0, 0);"
/>
</div>
</div>
<div
class="ant-color-picker-slider-container"
>
<div
class="ant-color-picker-slider-group ant-color-picker-slider-group-disabled-alpha"
>
<div
class="ant-color-picker-slider ant-color-picker-slider-hue"
>
<div
class="ant-color-picker-palette"
style="position: relative;"
>
<div
style="position: absolute; left: 0px; top: 0px; z-index: 1;"
>
<div
class="ant-color-picker-handler ant-color-picker-handler-sm"
style="background-color: rgb(0, 0, 0);"
/>
</div>
<div
class="ant-color-picker-gradient"
style="position: absolute; inset: 0;"
/>
</div>
</div>
</div>
<div
class="ant-color-picker-color-block"
>
<div
class="ant-color-picker-color-block-inner"
style="background: rgb(22, 119, 255);"
/>
</div>
</div>
</div>
<div
class="ant-color-picker-input-container"
>
<div
class="ant-select ant-select-sm ant-select-borderless ant-color-picker-format-select ant-select-single ant-select-show-arrow"
>
<div
class="ant-select-selector"
>
<span
class="ant-select-selection-search"
>
<input
aria-activedescendant="rc_select_TEST_OR_SSR_list_0"
aria-autocomplete="list"
aria-controls="rc_select_TEST_OR_SSR_list"
aria-expanded="false"
aria-haspopup="listbox"
aria-owns="rc_select_TEST_OR_SSR_list"
autocomplete="off"
class="ant-select-selection-search-input"
id="rc_select_TEST_OR_SSR"
readonly=""
role="combobox"
style="opacity: 0;"
type="search"
unselectable="on"
value=""
/>
</span>
<span
class="ant-select-selection-item"
title="HEX"
>
HEX
</span>
</div>
<div
class="ant-select-dropdown ant-slide-up-appear ant-slide-up-appear-prepare ant-slide-up ant-select-dropdown-placement-bottomRight"
style="--arrow-x: 0px; --arrow-y: 0px; left: -1000vw; top: -1000vh; box-sizing: border-box; width: 68px;"
>
<div>
<div
id="rc_select_TEST_OR_SSR_list"
role="listbox"
style="height: 0px; width: 0px; overflow: hidden;"
>
<div
aria-label="HEX"
aria-selected="true"
id="rc_select_TEST_OR_SSR_list_0"
role="option"
>
hex
</div>
<div
aria-label="HSB"
aria-selected="false"
id="rc_select_TEST_OR_SSR_list_1"
role="option"
>
hsb
</div>
</div>
<div
class="rc-virtual-list"
style="position: relative;"
>
<div
class="rc-virtual-list-holder"
style="max-height: 256px; overflow-y: auto;"
>
<div>
<div
class="rc-virtual-list-holder-inner"
style="display: flex; flex-direction: column;"
>
<div
aria-selected="true"
class="ant-select-item ant-select-item-option ant-select-item-option-active ant-select-item-option-selected"
title="HEX"
>
<div
class="ant-select-item-option-content"
>
HEX
</div>
<span
aria-hidden="true"
class="ant-select-item-option-state"
style="user-select: none;"
unselectable="on"
/>
</div>
<div
aria-selected="false"
class="ant-select-item ant-select-item-option"
title="HSB"
>
<div
class="ant-select-item-option-content"
>
HSB
</div>
<span
aria-hidden="true"
class="ant-select-item-option-state"
style="user-select: none;"
unselectable="on"
/>
</div>
<div
aria-selected="false"
class="ant-select-item ant-select-item-option"
title="RGB"
>
<div
class="ant-select-item-option-content"
>
RGB
</div>
<span
aria-hidden="true"
class="ant-select-item-option-state"
style="user-select: none;"
unselectable="on"
/>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<span
aria-hidden="true"
class="ant-select-arrow"
style="user-select: none;"
unselectable="on"
>
<span
aria-label="down"
class="anticon anticon-down ant-select-suffix"
role="img"
>
<svg
aria-hidden="true"
data-icon="down"
fill="currentColor"
focusable="false"
height="1em"
viewBox="64 64 896 896"
width="1em"
>
<path
d="M884 256h-75c-5.1 0-9.9 2.5-12.9 6.6L512 654.2 227.9 262.6c-3-4.1-7.8-6.6-12.9-6.6h-75c-6.5 0-10.3 7.4-6.5 12.7l352.6 486.1c12.8 17.6 39 17.6 51.7 0l352.6-486.1c3.9-5.3.1-12.7-6.4-12.7z"
/>
</svg>
</span>
</span>
</div>
<div
class="ant-color-picker-input"
>
<span
class="ant-input-affix-wrapper ant-color-picker-hex-input ant-input-affix-wrapper-sm"
>
<span
class="ant-input-prefix"
>
#
</span>
<input
class="ant-input ant-input-sm"
type="text"
value="1677FF"
/>
</span>
</div>
</div>
</div>
</div>
</div>
</div>
</div>,
]
`;
exports[`renders components/color-picker/demo/format.tsx extend context correctly 1`] = `
<div
class="ant-space ant-space-vertical"

15
components/color-picker/__tests__/__snapshots__/demo.test.ts.snap

@ -84,6 +84,21 @@ exports[`renders components/color-picker/demo/disabled.tsx correctly 1`] = `
</div>
`;
exports[`renders components/color-picker/demo/disabled-alpha.tsx correctly 1`] = `
<div
class="ant-color-picker-trigger"
>
<div
class="ant-color-picker-color-block"
>
<div
class="ant-color-picker-color-block-inner"
style="background:rgb(22, 119, 255)"
/>
</div>
</div>
`;
exports[`renders components/color-picker/demo/format.tsx correctly 1`] = `
<div
class="ant-space ant-space-vertical"

53
components/color-picker/__tests__/index.test.tsx

@ -4,9 +4,11 @@ import React, { useMemo, useState } from 'react';
import mountTest from '../../../tests/shared/mountTest';
import rtlTest from '../../../tests/shared/rtlTest';
import { waitFakeTimer } from '../../../tests/utils';
import { resetWarned } from '../../_util/warning';
import ConfigProvider from '../../config-provider';
import Form from '../../form';
import theme from '../../theme';
import type { ColorPickerProps } from '../ColorPicker';
import ColorPicker from '../ColorPicker';
import type { Color } from '../color';
@ -35,11 +37,14 @@ function doMouseMove(
describe('ColorPicker', () => {
mountTest(ColorPicker);
rtlTest(ColorPicker);
const errorSpy = jest.spyOn(console, 'error').mockImplementation(() => {});
beforeEach(() => {
resetWarned();
jest.useFakeTimers();
});
afterEach(() => {
errorSpy.mockReset();
jest.useRealTimers();
});
@ -458,4 +463,52 @@ describe('ColorPicker', () => {
doMouseMove(container, 0, 999);
expect(handleChangeComplete).toHaveBeenCalledTimes(1);
});
it('Should disabledAlpha work', async () => {
const { container } = render(<ColorPicker open disabledAlpha />);
expect(container.querySelector('.ant-color-picker-slider-group-disabled-alpha')).toBeTruthy();
expect(container.querySelector('.ant-color-picker-slider-alpha')).toBeFalsy();
expect(container.querySelector('.ant-color-picker-alpha-input')).toBeFalsy();
});
it('Should disabledAlpha work with value', async () => {
spyElementPrototypes(HTMLElement, {
getBoundingClientRect: () => ({
x: 0,
y: 100,
width: 100,
height: 100,
}),
});
const Demo = () => {
const [value, setValue] = useState<ColorPickerProps['value']>('#1677ff86');
const [changedValue, setChangedValue] = useState<ColorPickerProps['value']>('#1677ff86');
return (
<ColorPicker
open
disabledAlpha
value={value}
onChange={setValue}
onChangeComplete={setChangedValue}
>
<div className="color-value">
{typeof value === 'string' ? value : value?.toHexString()}
</div>
<div className="color-value-changed">
{typeof changedValue === 'string' ? changedValue : changedValue?.toHexString()}
</div>
</ColorPicker>
);
};
const { container } = render(<Demo />);
expect(container.querySelector('.color-value')?.innerHTML).toEqual('#1677ff86');
doMouseMove(container, 0, 999);
expect(container.querySelector('.color-value')?.innerHTML).toEqual('#000000');
expect(container.querySelector('.color-value-changed')?.innerHTML).toEqual('#000000');
});
it('Should warning work when set disabledAlpha true and color is alpha color', () => {
render(<ColorPicker disabledAlpha value="#1677ff" />);
expect(errorSpy).not.toHaveBeenCalled();
});
});

8
components/color-picker/components/ColorInput.tsx

@ -11,7 +11,7 @@ import ColorHsbInput from './ColorHsbInput';
import ColorRgbInput from './ColorRgbInput';
interface ColorInputProps
extends Pick<ColorPickerBaseProps, 'prefixCls' | 'format' | 'onFormatChange'> {
extends Pick<ColorPickerBaseProps, 'prefixCls' | 'format' | 'onFormatChange' | 'disabledAlpha'> {
value?: Color;
onChange?: (value: Color) => void;
}
@ -22,7 +22,7 @@ const selectOptions = [ColorFormat.hex, ColorFormat.hsb, ColorFormat.rgb].map((f
}));
const ColorInput: FC<ColorInputProps> = (props) => {
const { prefixCls, format, value, onFormatChange, onChange } = props;
const { prefixCls, format, value, disabledAlpha, onFormatChange, onChange } = props;
const [colorFormat, setColorFormat] = useMergedState(ColorFormat.hex, {
value: format,
onChange: onFormatChange,
@ -61,7 +61,9 @@ const ColorInput: FC<ColorInputProps> = (props) => {
options={selectOptions}
/>
<div className={colorInputPrefixCls}>{steppersNode}</div>
<ColorAlphaInput prefixCls={prefixCls} value={value} onChange={onChange} />
{!disabledAlpha && (
<ColorAlphaInput prefixCls={prefixCls} value={value} onChange={onChange} />
)}
</div>
);
};

12
components/color-picker/components/PanelPicker.tsx

@ -11,7 +11,7 @@ import ColorInput from './ColorInput';
export interface PanelPickerProps
extends Pick<
ColorPickerBaseProps,
'prefixCls' | 'colorCleared' | 'allowClear' | 'onChangeComplete'
'prefixCls' | 'colorCleared' | 'allowClear' | 'disabledAlpha' | 'onChangeComplete'
> {
value?: Color;
onChange?: (value?: Color, type?: HsbaColorType, pickColor?: boolean) => void;
@ -24,6 +24,7 @@ const PanelPicker: FC = () => {
colorCleared,
allowClear,
value,
disabledAlpha,
onChange,
onClear,
onChangeComplete,
@ -46,10 +47,17 @@ const PanelPicker: FC = () => {
<RcColorPicker
prefixCls={prefixCls}
value={value?.toHsb()}
disabledAlpha={disabledAlpha}
onChange={(colorValue, type) => onChange?.(colorValue, type, true)}
onChangeComplete={onChangeComplete}
/>
<ColorInput value={value} onChange={onChange} prefixCls={prefixCls} {...injectProps} />
<ColorInput
value={value}
onChange={onChange}
prefixCls={prefixCls}
disabledAlpha={disabledAlpha}
{...injectProps}
/>
</>
);
};

7
components/color-picker/demo/disabled-alpha.md

@ -0,0 +1,7 @@
## zh-CN
禁用颜色透明度。
## en-US
Disabled color alpha.

6
components/color-picker/demo/disabled-alpha.tsx

@ -0,0 +1,6 @@
import { ColorPicker } from 'antd';
import React from 'react';
const Demo = () => <ColorPicker disabledAlpha />;
export default Demo;

2
components/color-picker/index.en-US.md

@ -25,6 +25,7 @@ Used when the user needs to customize the color selection.
<code src="./demo/change-completed.tsx">Color change completed</code>
<code src="./demo/text-render.tsx">Rendering Trigger Text</code>
<code src="./demo/disabled.tsx">Disable</code>
<code src="./demo/disabled-alpha.tsx">Disabled Alpha</code>
<code src="./demo/allowClear.tsx">Clear Color</code>
<code src="./demo/trigger.tsx">Custom Trigger</code>
<code src="./demo/trigger-event.tsx">Custom Trigger Event</code>
@ -45,6 +46,7 @@ Used when the user needs to customize the color selection.
| children | Trigger of ColorPicker | React.ReactNode | - | |
| defaultValue | Default value of color | string \| `Color` | - | |
| disabled | Disable ColorPicker | boolean | - | |
| disabledAlpha | Disable Alpha | boolean | - | 5.8.0 |
| destroyTooltipOnHide | Whether destroy popover when hidden | `boolean` | false | 5.7.0 |
| format | Format of color | `rgb` \| `hex` \| `hsb` | `hex` | |
| open | Whether to show popup | boolean | - | |

2
components/color-picker/index.zh-CN.md

@ -26,6 +26,7 @@ group:
<code src="./demo/change-completed.tsx">颜色完成选择</code>
<code src="./demo/text-render.tsx">渲染触发器文本</code>
<code src="./demo/disabled.tsx">禁用</code>
<code src="./demo/disabled-alpha.tsx">禁用透明度</code>
<code src="./demo/allowClear.tsx">清除颜色</code>
<code src="./demo/trigger.tsx">自定义触发器</code>
<code src="./demo/trigger-event.tsx">自定义触发事件</code>
@ -46,6 +47,7 @@ group:
| children | 颜色选择器的触发器 | React.ReactNode | - | |
| defaultValue | 颜色默认的值 | string \| `Color` | - | |
| disabled | 禁用颜色选择器 | boolean | - | |
| disabledAlpha | 禁用透明度 | boolean | - | 5.8.0 |
| destroyTooltipOnHide | 关闭后是否销毁弹窗 | `boolean` | false | 5.7.0 |
| format | 颜色格式 | `rgb` \| `hex` \| `hsb` | `hex` | |
| open | 是否显示弹出窗口 | boolean | - | |

1
components/color-picker/interface.ts

@ -28,6 +28,7 @@ export interface ColorPickerBaseProps {
allowClear?: boolean;
colorCleared?: boolean;
disabled?: boolean;
disabledAlpha?: boolean;
presets?: PresetsItem[];
panelRender?: ColorPickerProps['panelRender'];
onFormatChange?: ColorPickerProps['onFormatChange'];

11
components/color-picker/style/picker.ts

@ -58,14 +58,23 @@ const genPickerStyle: GenerateStyle<ColorPickerToken, CSSObject> = (token) => {
boxShadow: colorPickerInsetShadow,
},
'&-alpha': getTransBg(`${colorPickerSliderHeight}px`, token.colorFillSecondary),
marginBottom: marginSM,
'&-hue': { marginBottom: marginSM },
},
[`${componentCls}-slider-container`]: {
display: 'flex',
gap: marginSM,
marginBottom: marginSM,
[`${componentCls}-slider-group`]: {
flex: 1,
'&-disabled-alpha': {
display: 'flex',
alignItems: 'center',
[`${componentCls}-slider`]: {
flex: 1,
marginBottom: 0,
},
},
},
},
};

6
components/color-picker/util.ts

@ -14,3 +14,9 @@ export const generateColor = (color: ColorGenInput<Color>): Color => {
export const getRoundNumber = (value: number) => Math.round(Number(value || 0));
export const getAlphaColor = (color: Color) => getRoundNumber(color.toHsb().a * 100);
export const genAlphaColor = (color: Color, alpha?: number) => {
const hsba = color.toHsb();
hsba.a = alpha || 1;
return generateColor(hsba);
};

25
components/modal/ConfirmDialog.tsx

@ -16,6 +16,12 @@ import type { ModalFuncProps, ModalLocale } from './interface';
interface ConfirmDialogProps extends ModalFuncProps {
afterClose?: () => void;
close?: (...args: any[]) => void;
/**
* `close` prop support `...args` that pass to the developer
* that we can not break this.
* Provider `onClose` for internal usage
*/
onConfirm?: (confirmed: boolean) => void;
autoFocusButton?: null | 'ok' | 'cancel';
rootPrefixCls: string;
iconPrefixCls?: string;
@ -23,6 +29,11 @@ interface ConfirmDialogProps extends ModalFuncProps {
/** @private Internal Usage. Do not override this */
locale?: ModalLocale;
/**
* Do not throw if is await mode
*/
isSilent?: () => boolean;
}
export function ConfirmContent(
@ -35,6 +46,8 @@ export function ConfirmContent(
onCancel,
onOk,
close,
onConfirm,
isSilent,
okText,
okButtonProps,
cancelText,
@ -89,8 +102,12 @@ export function ConfirmContent(
const cancelButton = mergedOkCancel && (
<ActionButton
isSilent={isSilent}
actionFn={onCancel}
close={close}
close={(...args: any[]) => {
close?.(...args);
onConfirm?.(false);
}}
autoFocus={autoFocusButton === 'cancel'}
buttonProps={cancelButtonProps}
prefixCls={`${rootPrefixCls}-btn`}
@ -112,9 +129,13 @@ export function ConfirmContent(
<div className={`${confirmPrefixCls}-btns`}>
{cancelButton}
<ActionButton
isSilent={isSilent}
type={okType}
actionFn={onOk}
close={close}
close={(...args: any[]) => {
close?.(...args);
onConfirm?.(true);
}}
autoFocus={autoFocusButton === 'ok'}
buttonProps={okButtonProps}
prefixCls={`${rootPrefixCls}-btn`}

44
components/modal/__tests__/hook.test.tsx

@ -367,4 +367,48 @@ describe('Modal.hook', () => {
expect(afterClose).toHaveBeenCalledTimes(1);
});
it('support await', async () => {
jest.useFakeTimers();
let notReady = true;
let lastResult: boolean | null = null;
const Demo = () => {
const [modal, contextHolder] = Modal.useModal();
React.useEffect(() => {
(async () => {
lastResult = await modal.confirm({
content: <Input />,
onOk: async () => {
if (notReady) {
notReady = false;
return Promise.reject();
}
},
});
})();
}, []);
return contextHolder;
};
render(<Demo />);
// Wait for modal show
await waitFakeTimer();
// First time click should not close
fireEvent.click(document.querySelector('.ant-btn-primary')!);
await waitFakeTimer();
expect(lastResult).toBeFalsy();
// Second time click to close
fireEvent.click(document.querySelector('.ant-btn-primary')!);
await waitFakeTimer();
expect(lastResult).toBeTruthy();
jest.useRealTimers();
});
});

4
components/modal/demo/hooks.md

@ -1,7 +1,7 @@
## zh-CN
通过 `Modal.useModal` 创建支持读取 context 的 `contextHolder`
通过 `Modal.useModal` 创建支持读取 context 的 `contextHolder`其中仅有 hooks 方法支持 Promise `await` 操作。
## en-US
Use `Modal.useModal` to get `contextHolder` with context accessible issue.
Use `Modal.useModal` to get `contextHolder` with context accessible issue. Only hooks method support Promise `await` operation.

9
components/modal/demo/hooks.tsx

@ -22,8 +22,9 @@ const App: React.FC = () => {
<ReachableContext.Provider value="Light">
<Space>
<Button
onClick={() => {
modal.confirm(config);
onClick={async () => {
const confirmed = await modal.confirm(config);
console.log('Confirmed: ', confirmed);
}}
>
Confirm
@ -36,14 +37,14 @@ const App: React.FC = () => {
Warning
</Button>
<Button
onClick={() => {
onClick={async () => {
modal.info(config);
}}
>
Info
</Button>
<Button
onClick={() => {
onClick={async () => {
modal.error(config);
}}
>

15
components/modal/index.en-US.md

@ -22,16 +22,16 @@ Additionally, if you need show a simple confirmation dialog, you can use [`App.u
<code src="./demo/basic.tsx">Basic</code>
<code src="./demo/async.tsx">Asynchronously close</code>
<code src="./demo/footer.tsx">Customized Footer</code>
<code src="./demo/confirm.tsx">Confirmation modal dialog</code>
<code src="./demo/hooks.tsx">Use hooks to get context</code>
<code src="./demo/locale.tsx">Internationalization</code>
<code src="./demo/manual.tsx">Manual to update destroy</code>
<code src="./demo/position.tsx">To customize the position of modal</code>
<code src="./demo/dark.tsx" debug>Dark Bg</code>
<code src="./demo/button-props.tsx">Customize footer buttons props</code>
<code src="./demo/hooks.tsx">Use hooks to get context</code>
<code src="./demo/modal-render.tsx">Custom modal content render</code>
<code src="./demo/width.tsx">To customize the width of modal</code>
<code src="./demo/static-info.tsx">Static Method</code>
<code src="./demo/confirm.tsx">Static confirmation</code>
<code src="./demo/confirm-router.tsx">destroy confirmation modal dialog</code>
<code src="./demo/render-panel.tsx" debug>\_InternalPanelDoNotUseOrYouWillBeFired</code>
<code src="./demo/custom-mouse-position.tsx" debug>Control modal's animation origin position</code>
@ -167,6 +167,17 @@ React.useEffect(() => {
return <div>{contextHolder}</div>;
```
`modal.confirm` return method:
- `destroy`: Destroy current modal
- `update`: Update current modal
- `then`: (Hooks only) Promise chain call, support `await` operation
```tsx
// Return `true` when click `onOk` and `false` when click `onCancel`
const confirmed = await modal.confirm({ ... });
```
## Design Token
<ComponentTokenTable component="Modal"></ComponentTokenTable>

15
components/modal/index.zh-CN.md

@ -23,16 +23,16 @@ demo:
<code src="./demo/basic.tsx">基本</code>
<code src="./demo/async.tsx">异步关闭</code>
<code src="./demo/footer.tsx">自定义页脚</code>
<code src="./demo/confirm.tsx">确认对话框</code>
<code src="./demo/hooks.tsx">使用 hooks 获得上下文</code>
<code src="./demo/locale.tsx">国际化</code>
<code src="./demo/manual.tsx">手动更新和移除</code>
<code src="./demo/position.tsx">自定义位置</code>
<code src="./demo/dark.tsx" debug>暗背景</code>
<code src="./demo/button-props.tsx">自定义页脚按钮属性</code>
<code src="./demo/hooks.tsx">使用 hooks 获得上下文</code>
<code src="./demo/modal-render.tsx">自定义渲染对话框</code>
<code src="./demo/width.tsx">自定义模态的宽度</code>
<code src="./demo/static-info.tsx">静态方法</code>
<code src="./demo/confirm.tsx">静态确认对话框</code>
<code src="./demo/confirm-router.tsx">销毁确认对话框</code>
<code src="./demo/render-panel.tsx" debug>\_InternalPanelDoNotUseOrYouWillBeFired</code>
<code src="./demo/custom-mouse-position.tsx" debug>控制弹框动画原点</code>
@ -168,6 +168,17 @@ React.useEffect(() => {
return <div>{contextHolder}</div>;
```
`modal.confirm` 返回方法:
- `destroy`:销毁当前窗口
- `update`:更新当前窗口
- `then`:Promise 链式调用,支持 `await` 操作。该方法为 Hooks 仅有
```tsx
//点击 `onOk` 时返回 `true`,点击 `onCancel` 时返回 `false`
const confirmed = await modal.confirm({ ... });
```
## Design Token
<ComponentTokenTable component="Modal"></ComponentTokenTable>

8
components/modal/useModal/HookModal.tsx

@ -8,6 +8,11 @@ import type { ModalFuncProps } from '../interface';
export interface HookModalProps {
afterClose: () => void;
config: ModalFuncProps;
onConfirm?: (confirmed: boolean) => void;
/**
* Do not throw if is await mode
*/
isSilent?: () => boolean;
}
export interface HookModalRef {
@ -16,7 +21,7 @@ export interface HookModalRef {
}
const HookModal: React.ForwardRefRenderFunction<HookModalRef, HookModalProps> = (
{ afterClose: hookAfterClose, config },
{ afterClose: hookAfterClose, config, ...restProps },
ref,
) => {
const [open, setOpen] = React.useState(true);
@ -66,6 +71,7 @@ const HookModal: React.ForwardRefRenderFunction<HookModalRef, HookModalProps> =
}
direction={innerConfig.direction || direction}
cancelText={innerConfig.cancelText || contextLocale?.cancelText}
{...restProps}
/>
);
};

35
components/modal/useModal/index.tsx

@ -1,6 +1,6 @@
import * as React from 'react';
import usePatchElement from '../../_util/hooks/usePatchElement';
import type { ModalStaticFunctions } from '../confirm';
import type { ModalFunc, ModalStaticFunctions } from '../confirm';
import { withConfirm, withError, withInfo, withSuccess, withWarn } from '../confirm';
import destroyFns from '../destroyFns';
import type { ModalFuncProps } from '../interface';
@ -13,6 +13,13 @@ interface ElementsHolderRef {
patchElement: ReturnType<typeof usePatchElement>[1];
}
// Add `then` field for `ModalFunc` return instance.
export type ModalFuncWithPromise = (...args: Parameters<ModalFunc>) => ReturnType<ModalFunc> & {
then<T>(resolve: (confirmed: boolean) => T, reject: VoidFunction): Promise<T>;
};
export type HookAPI = Omit<Record<keyof ModalStaticFunctions, ModalFuncWithPromise>, 'warn'>;
const ElementsHolder = React.memo(
React.forwardRef<ElementsHolderRef>((_props, ref) => {
const [elements, patchElement] = usePatchElement();
@ -28,10 +35,7 @@ const ElementsHolder = React.memo(
}),
);
function useModal(): readonly [
instance: Omit<ModalStaticFunctions, 'warn'>,
contextHolder: React.ReactElement,
] {
function useModal(): readonly [instance: HookAPI, contextHolder: React.ReactElement] {
const holderRef = React.useRef<ElementsHolderRef>(null);
// ========================== Effect ==========================
@ -56,6 +60,13 @@ function useModal(): readonly [
const modalRef = React.createRef<HookModalRef>();
// Proxy to promise with `onClose`
let resolvePromise: (confirmed: boolean) => void;
const promise = new Promise<boolean>((resolve) => {
resolvePromise = resolve;
});
let silent = false;
let closeFunc: Function | undefined;
const modal = (
<HookModal
@ -65,6 +76,10 @@ function useModal(): readonly [
afterClose={() => {
closeFunc?.();
}}
isSilent={() => silent}
onConfirm={(confirmed) => {
resolvePromise(confirmed);
}}
/>
);
@ -74,7 +89,7 @@ function useModal(): readonly [
destroyFns.push(closeFunc);
}
return {
const instance: ReturnType<ModalFuncWithPromise> = {
destroy: () => {
function destroyAction() {
modalRef.current?.destroy();
@ -97,12 +112,18 @@ function useModal(): readonly [
setActionQueue((prev) => [...prev, updateAction]);
}
},
then: (resolve) => {
silent = true;
return promise.then(resolve);
},
};
return instance;
},
[],
);
const fns = React.useMemo<Omit<ModalStaticFunctions, 'warn'>>(
const fns = React.useMemo<HookAPI>(
() => ({
info: getConfirmFunc(withInfo),
success: getConfirmFunc(withSuccess),

Loading…
Cancel
Save