chore: auto merge branches (#50216)

chore: merge feature into master
This commit is contained in:
github-actions[bot] 2024-08-03 03:48:53 +00:00 committed by GitHub
commit d9b550a25d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
181 changed files with 6904 additions and 2111 deletions

View File

@ -7,7 +7,7 @@ import classNames from 'classnames';
import { PRESET_COLORS } from './colorUtil';
type Color = GetProp<ColorPickerProps, 'value'>;
type Color = Extract<GetProp<ColorPickerProps, 'value'>, string | { cleared: any }>;
const useStyle = createStyles(({ token, css }) => ({
color: css`

View File

@ -40,7 +40,7 @@ import RadiusPicker from './RadiusPicker';
import type { THEME } from './ThemePicker';
import ThemePicker from './ThemePicker';
type Color = GetProp<ColorPickerProps, 'value'>;
type Color = Extract<GetProp<ColorPickerProps, 'value'>, string | { cleared: any }>;
const { Header, Content, Sider } = Layout;

View File

@ -18,13 +18,8 @@ export type CustomComponent<P = AnyObject> = React.ComponentType<P> | string;
* ```
* @since 5.13.0
*/
export type GetProps<T extends React.ComponentType<any> | object> = T extends React.ComponentType<
infer P
>
? P
: T extends object
? T
: never;
export type GetProps<T extends React.ComponentType<any> | object> =
T extends React.ComponentType<infer P> ? P : T extends object ? T : never;
/**
* Get component props by component name
@ -71,3 +66,10 @@ export type GetRef<T extends ReactRefComponent<any> | React.Component<any>> =
: T extends React.ComponentType<infer P>
? ExtractRefAttributesRef<P>
: never;
export type GetContextProps<T> = T extends React.Context<infer P> ? P : never;
export type GetContextProp<
T extends React.Context<any>,
PropName extends keyof GetContextProps<T>,
> = NonNullable<GetContextProps<T>[PropName]>;

View File

@ -1,12 +1,7 @@
import { Keyframes, unit } from '@ant-design/cssinjs';
import { resetComponent } from '../../style';
import type {
FullToken,
GenerateStyle,
GenStyleFn,
GetDefaultToken,
} from '../../theme/internal';
import type { FullToken, GenerateStyle, GenStyleFn, GetDefaultToken } from '../../theme/internal';
import { genPresetColor, genStyleHooks, mergeToken } from '../../theme/internal';
/** Component only token. Which will handle additional calculation of alias token */
@ -21,12 +16,12 @@ export interface ComponentToken {
* @desc
* @descEN Height of badge
*/
indicatorHeight: number;
indicatorHeight: number | string;
/**
* @desc
* @descEN Height of small badge
*/
indicatorHeightSM: number;
indicatorHeightSM: number | string;
/**
* @desc
* @descEN Size of dot badge

View File

@ -1,10 +1,6 @@
import type { CSSProperties } from 'react';
import type {
FullToken,
GetDefaultToken,
GenStyleFn,
} from '../../theme/internal';
import type { FullToken, GetDefaultToken, GenStyleFn } from '../../theme/internal';
import { getLineHeight, mergeToken } from '../../theme/internal';
/** Component only token. Which will handle additional calculation of alias token */

View File

@ -16,17 +16,17 @@ export interface ComponentToken {
* @desc
* @descEN Width of year select
*/
yearControlWidth: number;
yearControlWidth: number | string;
/**
* @desc
* @descEN Width of month select
*/
monthControlWidth: number;
monthControlWidth: number | string;
/**
* @desc
* @descEN Height of mini calendar content
*/
miniContentHeight: number;
miniContentHeight: number | string;
/**
* @desc
* @descEN Background color of full calendar
@ -46,9 +46,9 @@ export interface ComponentToken {
interface CalendarToken extends FullToken<'Calendar'>, PickerPanelToken, PanelComponentToken {
calendarCls: string;
dateValueHeight: number;
weekHeight: number;
dateContentHeight: number;
dateValueHeight: number | string;
weekHeight: number | string;
dateContentHeight: number | string;
}
export const genCalendarStyles = (token: CalendarToken): CSSObject => {

View File

@ -15,22 +15,22 @@ export interface ComponentToken {
* @desc
* @descEN Font size of card header
*/
headerFontSize: number;
headerFontSize: number | string;
/**
* @desc
* @descEN Font size of small card header
*/
headerFontSizeSM: number;
headerFontSizeSM: number | string;
/**
* @desc
* @descEN Height of card header
*/
headerHeight: number;
headerHeight: number | string;
/**
* @desc
* @descEN Height of small card header
*/
headerHeightSM: number;
headerHeightSM: number | string;
/**
* @desc
* @descEN Background color of card actions

View File

@ -31,6 +31,8 @@ export interface CarouselRef {
innerSlider: any;
}
const dotsClass = 'slick-dots';
interface ArrowType extends React.ButtonHTMLAttributes<HTMLButtonElement> {
currentSlide?: number;
slideCount?: number;
@ -40,8 +42,6 @@ const ArrowButton: React.FC<ArrowType> = ({ currentSlide, slideCount, ...rest })
<button type="button" {...rest} />
);
const dotsClass = 'slick-dots';
const Carousel = React.forwardRef<CarouselRef, CarouselProps>((props, ref) => {
const {
dots = true,

View File

@ -9,12 +9,12 @@ export interface ComponentToken {
* @desc
* @descEN Width of indicator
*/
dotWidth: number;
dotWidth: number | string;
/**
* @desc
* @descEN Height of indicator
*/
dotHeight: number;
dotHeight: number | string;
/**
* @desc
* @descEN gap between indicator
@ -31,7 +31,7 @@ export interface ComponentToken {
* @desc
* @descEN Width of active indicator
*/
dotActiveWidth: number;
dotActiveWidth: number | string;
/**
* @desc
* @descEN Size of arrows

View File

@ -10,17 +10,17 @@ export interface ComponentToken {
* @desc
* @descEN Width of Cascader
*/
controlWidth: number;
controlWidth: number | string;
/**
* @desc
* @descEN Width of item
*/
controlItemWidth: number;
controlItemWidth: number | string;
/**
* @desc
* @descEN Height of dropdown
*/
dropdownHeight: number;
dropdownHeight: number | string;
/**
* @desc
* @descEN Background color of selected item

View File

@ -1,4 +1,4 @@
import React, { useContext, useMemo, useRef } from 'react';
import React, { useContext, useMemo } from 'react';
import classNames from 'classnames';
import useMergedState from 'rc-util/lib/hooks/useMergedState';
@ -14,14 +14,14 @@ import useSize from '../config-provider/hooks/useSize';
import { FormItemInputContext } from '../form/context';
import type { PopoverProps } from '../popover';
import Popover from '../popover';
import type { Color } from './color';
import { AggregationColor } from './color';
import type { ColorPickerPanelProps } from './ColorPickerPanel';
import ColorPickerPanel from './ColorPickerPanel';
import ColorTrigger from './components/ColorTrigger';
import useColorState from './hooks/useColorState';
import type { ColorPickerBaseProps, ColorPickerProps, TriggerPlacement } from './interface';
import useModeColor from './hooks/useModeColor';
import type { ColorPickerProps, ModeType, TriggerPlacement } from './interface';
import useStyle from './style';
import { genAlphaColor, generateColor, getAlphaColor } from './util';
import { genAlphaColor, generateColor, getColorAlpha } from './util';
type CompoundedComponent = React.FC<ColorPickerProps> & {
_InternalPanelDoNotUseOrYouWillBeFired: typeof PurePanel;
@ -29,6 +29,7 @@ type CompoundedComponent = React.FC<ColorPickerProps> & {
const ColorPicker: CompoundedComponent = (props) => {
const {
mode,
value,
defaultValue,
format,
@ -65,10 +66,6 @@ const ColorPicker: CompoundedComponent = (props) => {
const contextDisabled = useContext(DisabledContext);
const mergedDisabled = disabled ?? contextDisabled;
const [colorValue, setColorValue, prevValue] = useColorState('', {
value,
defaultValue,
});
const [popupOpen, setPopupOpen] = useMergedState(false, {
value: open,
postState: (openData) => !mergedDisabled && openData,
@ -82,9 +79,91 @@ const ColorPicker: CompoundedComponent = (props) => {
const prefixCls = getPrefixCls('color-picker', customizePrefixCls);
const isAlphaColor = useMemo(() => getAlphaColor(colorValue) < 100, [colorValue]);
// ================== Value & Mode =================
const [mergedColor, setColor, modeState, setModeState, modeOptions] = useModeColor(
defaultValue,
value,
mode,
);
// ===================== Form Status =====================
const isAlphaColor = useMemo(() => getColorAlpha(mergedColor) < 100, [mergedColor]);
// ==================== Change =====================
// To enhance user experience, we cache the gradient color when switch from gradient to single
// If user not modify single color, we will use the cached gradient color.
const [cachedGradientColor, setCachedGradientColor] = React.useState<AggregationColor | null>(
null,
);
const onInternalChangeComplete: ColorPickerProps['onChangeComplete'] = (color) => {
if (onChangeComplete) {
let changeColor = generateColor(color);
// ignore alpha color
if (disabledAlpha && isAlphaColor) {
changeColor = genAlphaColor(color);
}
onChangeComplete(changeColor);
}
};
const onInternalChange: ColorPickerPanelProps['onChange'] = (data, pickColor) => {
let color: AggregationColor = generateColor(data as AggregationColor);
// ignore alpha color
if (disabledAlpha && isAlphaColor) {
color = genAlphaColor(color);
}
setColor(color);
setCachedGradientColor(null);
// Trigger change event
if (onChange) {
onChange(color, color.toCssString());
}
// Only for drag-and-drop color picking
if (!pickColor) {
onInternalChangeComplete(color);
}
};
// =================== Gradient ====================
const [activeIndex, setActiveIndex] = React.useState(0);
const [gradientDragging, setGradientDragging] = React.useState(false);
// Mode change should also trigger color change
const onInternalModeChange = (newMode: ModeType) => {
setModeState(newMode);
if (newMode === 'single' && mergedColor.isGradient()) {
setActiveIndex(0);
onInternalChange(new AggregationColor(mergedColor.getColors()[0].color));
// Should after `onInternalChange` since it will clear the cached color
setCachedGradientColor(mergedColor);
} else if (newMode === 'gradient' && !mergedColor.isGradient()) {
const baseColor = isAlphaColor ? genAlphaColor(mergedColor) : mergedColor;
onInternalChange(
new AggregationColor(
cachedGradientColor || [
{
percent: 0,
color: baseColor,
},
{
percent: 100,
color: baseColor,
},
],
),
);
}
};
// ================== Form Status ==================
const { status: contextStatus } = React.useContext(FormItemInputContext);
// ===================== Style =====================
@ -106,8 +185,6 @@ const ColorPicker: CompoundedComponent = (props) => {
);
const mergedPopupCls = classNames(prefixCls, mergedRootCls);
const popupAllowCloseRef = useRef(true);
// ===================== Warning ======================
if (process.env.NODE_ENV !== 'production') {
const warning = devUseWarning('ColorPicker');
@ -119,48 +196,6 @@ const ColorPicker: CompoundedComponent = (props) => {
);
}
const handleChange: ColorPickerPanelProps['onChange'] = (data, type, pickColor) => {
let color: Color = generateColor(data as Color);
// If color is cleared, reset alpha to 100
const isNull = value === null || (!value && defaultValue === null);
if (prevValue.current?.cleared || isNull) {
// ignore alpha slider
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;
} else {
onChangeComplete?.(color);
}
setColorValue(color);
onChange?.(color, color.toHexString());
};
const handleClear = () => {
onClear?.();
};
const handleChangeComplete: ColorPickerProps['onChangeComplete'] = (color) => {
popupAllowCloseRef.current = true;
let changeColor = generateColor(color);
// ignore alpha color
if (disabledAlpha && isAlphaColor) {
changeColor = genAlphaColor(color);
}
onChangeComplete?.(changeColor);
};
const popoverProps: PopoverProps = {
open: popupOpen,
trigger,
@ -172,19 +207,6 @@ const ColorPicker: CompoundedComponent = (props) => {
destroyTooltipOnHide,
};
const colorBaseProps: ColorPickerBaseProps = {
prefixCls,
color: colorValue,
allowClear,
disabled: mergedDisabled,
disabledAlpha,
presets,
panelRender,
format: formatValue,
onFormatChange: setFormatValue,
onChangeComplete: handleChangeComplete,
};
const mergedStyle: React.CSSProperties = { ...colorPicker?.style, ...style };
// ============================ zIndex ============================
@ -194,17 +216,32 @@ const ColorPicker: CompoundedComponent = (props) => {
style={styles?.popup}
overlayInnerStyle={styles?.popupOverlayInner}
onOpenChange={(visible) => {
if (popupAllowCloseRef.current && !mergedDisabled) {
if (!visible || !mergedDisabled) {
setPopupOpen(visible);
}
}}
content={
<ContextIsolator form>
<ColorPickerPanel
{...colorBaseProps}
onChange={handleChange}
onChangeComplete={handleChangeComplete}
onClear={handleClear}
mode={modeState}
onModeChange={onInternalModeChange}
modeOptions={modeOptions}
prefixCls={prefixCls}
value={mergedColor}
allowClear={allowClear}
disabled={mergedDisabled}
disabledAlpha={disabledAlpha}
presets={presets}
panelRender={panelRender}
format={formatValue}
onFormatChange={setFormatValue}
onChange={onInternalChange}
onChangeComplete={onInternalChangeComplete}
onClear={onClear}
activeIndex={activeIndex}
onActive={setActiveIndex}
gradientDragging={gradientDragging}
onGradientDragging={setGradientDragging}
/>
</ContextIsolator>
}
@ -213,6 +250,7 @@ const ColorPicker: CompoundedComponent = (props) => {
>
{children || (
<ColorTrigger
activeIndex={popupOpen ? activeIndex : -1}
open={popupOpen}
className={mergedCls}
style={mergedStyle}
@ -221,7 +259,7 @@ const ColorPicker: CompoundedComponent = (props) => {
showText={showText}
format={formatValue}
{...rest}
color={colorValue}
color={mergedColor}
/>
)}
</Popover>,

View File

@ -1,40 +1,91 @@
import type { FC } from 'react';
import React from 'react';
import type { HsbaColorType } from '@rc-component/color-picker';
import Divider from '../divider';
import type { Color } from './color';
import PanelPicker from './components/PanelPicker';
import PanelPresets from './components/PanelPresets';
import { PanelPickerProvider, PanelPresetsProvider } from './context';
import type { ColorPickerBaseProps } from './interface';
import { PanelPickerContext, PanelPresetsContext } from './context';
import type { PanelPickerContextProps, PanelPresetsContextProps } from './context';
import type { ColorPickerProps } from './interface';
export interface ColorPickerPanelProps extends ColorPickerBaseProps {
onChange?: (value?: Color, type?: HsbaColorType, pickColor?: boolean) => void;
export interface ColorPickerPanelProps
extends PanelPickerContextProps,
Omit<PanelPresetsContextProps, 'onChange'> {
onClear?: () => void;
panelRender?: ColorPickerProps['panelRender'];
}
const ColorPickerPanel: FC<ColorPickerPanelProps> = (props) => {
const { prefixCls, presets, panelRender, color, onChange, onClear, ...injectProps } = props;
const colorPickerPanelPrefixCls = `${prefixCls}-inner`;
// ==== Inject props ===
const panelPickerProps = {
const {
prefixCls,
value: color,
presets,
panelRender,
value,
onChange,
onClear,
...injectProps,
};
allowClear,
disabledAlpha,
mode,
onModeChange,
modeOptions,
onChangeComplete,
activeIndex,
onActive,
format,
onFormatChange,
gradientDragging,
onGradientDragging,
} = props;
const colorPickerPanelPrefixCls = `${prefixCls}-inner`;
const panelPresetsProps = React.useMemo(
// ===================== Context ======================
const panelContext: PanelPickerContextProps = React.useMemo(
() => ({
prefixCls,
value: color,
value,
onChange,
onClear,
allowClear,
disabledAlpha,
mode,
onModeChange,
modeOptions,
onChangeComplete,
activeIndex,
onActive,
format,
onFormatChange,
gradientDragging,
onGradientDragging,
}),
[
prefixCls,
value,
onChange,
onClear,
allowClear,
disabledAlpha,
mode,
onModeChange,
modeOptions,
onChangeComplete,
activeIndex,
onActive,
format,
onFormatChange,
gradientDragging,
onGradientDragging,
],
);
const presetContext: PanelPresetsContextProps = React.useMemo(
() => ({
prefixCls,
value,
presets,
onChange,
}),
[prefixCls, color, presets, onChange],
[prefixCls, value, presets, onChange],
);
// ====================== Render ======================
@ -47,8 +98,8 @@ const ColorPickerPanel: FC<ColorPickerPanelProps> = (props) => {
);
return (
<PanelPickerProvider value={panelPickerProps}>
<PanelPresetsProvider value={panelPresetsProps}>
<PanelPickerContext.Provider value={panelContext}>
<PanelPresetsContext.Provider value={presetContext}>
<div className={colorPickerPanelPrefixCls}>
{typeof panelRender === 'function'
? panelRender(innerPanel, {
@ -59,8 +110,8 @@ const ColorPickerPanel: FC<ColorPickerPanelProps> = (props) => {
})
: innerPanel}
</div>
</PanelPresetsProvider>
</PanelPickerProvider>
</PanelPresetsContext.Provider>
</PanelPickerContext.Provider>
);
};

View File

@ -9,7 +9,7 @@ exports[`renders components/color-picker/demo/allowClear.tsx correctly 1`] = `
>
<div
class="ant-color-picker-color-block-inner"
style="background:rgb(22, 119, 255)"
style="background:rgb(22,119,255)"
/>
</div>
</div>
@ -24,7 +24,7 @@ exports[`renders components/color-picker/demo/base.tsx correctly 1`] = `
>
<div
class="ant-color-picker-color-block-inner"
style="background:rgb(22, 119, 255)"
style="background:rgb(22,119,255)"
/>
</div>
</div>
@ -42,7 +42,7 @@ exports[`renders components/color-picker/demo/change-completed.tsx correctly 1`]
>
<div
class="ant-color-picker-color-block-inner"
style="background:rgb(22, 119, 255)"
style="background:rgb(22,119,255)"
/>
</div>
</div>
@ -58,7 +58,7 @@ exports[`renders components/color-picker/demo/controlled.tsx correctly 1`] = `
>
<div
class="ant-color-picker-color-block-inner"
style="background:rgb(22, 119, 255)"
style="background:rgb(22,119,255)"
/>
</div>
</div>
@ -73,7 +73,7 @@ exports[`renders components/color-picker/demo/disabled.tsx correctly 1`] = `
>
<div
class="ant-color-picker-color-block-inner"
style="background:rgb(22, 119, 255)"
style="background:rgb(22,119,255)"
/>
</div>
<div
@ -93,7 +93,7 @@ exports[`renders components/color-picker/demo/disabled-alpha.tsx correctly 1`] =
>
<div
class="ant-color-picker-color-block-inner"
style="background:rgb(22, 119, 255)"
style="background:rgb(22,119,255)"
/>
</div>
</div>
@ -121,7 +121,7 @@ exports[`renders components/color-picker/demo/format.tsx correctly 1`] = `
>
<div
class="ant-color-picker-color-block-inner"
style="background:rgb(22, 119, 255)"
style="background:rgb(22,119,255)"
/>
</div>
</div>
@ -154,7 +154,7 @@ exports[`renders components/color-picker/demo/format.tsx correctly 1`] = `
>
<div
class="ant-color-picker-color-block-inner"
style="background:rgb(23, 120, 255)"
style="background:rgb(23,120,255)"
/>
</div>
</div>
@ -187,7 +187,7 @@ exports[`renders components/color-picker/demo/format.tsx correctly 1`] = `
>
<div
class="ant-color-picker-color-block-inner"
style="background:rgb(22, 119, 255)"
style="background:rgb(22,119,255)"
/>
</div>
</div>
@ -206,6 +206,93 @@ exports[`renders components/color-picker/demo/format.tsx correctly 1`] = `
</div>
`;
exports[`renders components/color-picker/demo/line-gradient.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"
>
<div
class="ant-color-picker-trigger"
>
<div
class="ant-color-picker-color-block"
>
<div
class="ant-color-picker-color-block-inner"
style="background:linear-gradient(90deg, rgb(16,142,233) 0%, rgb(135,208,104) 100%)"
/>
</div>
<div
class="ant-color-picker-trigger-text"
>
<span
class="ant-color-picker-trigger-text-cell"
>
rgb(16,142,233)
<!-- -->
<!-- -->
0
<!-- -->
%
</span>
<span
class="ant-color-picker-trigger-text-cell"
>
rgb(135,208,104)
<!-- -->
<!-- -->
100
<!-- -->
%
</span>
</div>
</div>
</div>
<div
class="ant-space-item"
>
<div
class="ant-color-picker-trigger"
>
<div
class="ant-color-picker-color-block"
>
<div
class="ant-color-picker-color-block-inner"
style="background:linear-gradient(90deg, rgb(16,142,233) 0%, rgb(135,208,104) 100%)"
/>
</div>
<div
class="ant-color-picker-trigger-text"
>
<span
class="ant-color-picker-trigger-text-cell"
>
rgb(16,142,233)
<!-- -->
<!-- -->
0
<!-- -->
%
</span>
<span
class="ant-color-picker-trigger-text-cell"
>
rgb(135,208,104)
<!-- -->
<!-- -->
100
<!-- -->
%
</span>
</div>
</div>
</div>
</div>
`;
exports[`renders components/color-picker/demo/panel-render.tsx correctly 1`] = `
<div
class="ant-space ant-space-vertical ant-space-gap-row-small ant-space-gap-col-small"
@ -234,7 +321,7 @@ exports[`renders components/color-picker/demo/panel-render.tsx correctly 1`] = `
>
<div
class="ant-color-picker-color-block-inner"
style="background:rgb(22, 119, 255)"
style="background:rgb(22,119,255)"
/>
</div>
</div>
@ -265,7 +352,7 @@ exports[`renders components/color-picker/demo/panel-render.tsx correctly 1`] = `
>
<div
class="ant-color-picker-color-block-inner"
style="background:rgb(22, 119, 255)"
style="background:rgb(22,119,255)"
/>
</div>
</div>
@ -284,7 +371,7 @@ exports[`renders components/color-picker/demo/presets.tsx correctly 1`] = `
>
<div
class="ant-color-picker-color-block-inner"
style="background:rgb(22, 119, 255)"
style="background:rgb(22,119,255)"
/>
</div>
</div>
@ -306,7 +393,7 @@ exports[`renders components/color-picker/demo/pure-panel.tsx correctly 1`] = `
>
<div
class="ant-color-picker-color-block-inner"
style="background:rgb(22, 119, 255)"
style="background:rgb(22,119,255)"
/>
</div>
</div>
@ -335,7 +422,7 @@ exports[`renders components/color-picker/demo/size.tsx correctly 1`] = `
>
<div
class="ant-color-picker-color-block-inner"
style="background:rgb(22, 119, 255)"
style="background:rgb(22,119,255)"
/>
</div>
</div>
@ -351,7 +438,7 @@ exports[`renders components/color-picker/demo/size.tsx correctly 1`] = `
>
<div
class="ant-color-picker-color-block-inner"
style="background:rgb(22, 119, 255)"
style="background:rgb(22,119,255)"
/>
</div>
</div>
@ -367,7 +454,7 @@ exports[`renders components/color-picker/demo/size.tsx correctly 1`] = `
>
<div
class="ant-color-picker-color-block-inner"
style="background:rgb(22, 119, 255)"
style="background:rgb(22,119,255)"
/>
</div>
</div>
@ -391,7 +478,7 @@ exports[`renders components/color-picker/demo/size.tsx correctly 1`] = `
>
<div
class="ant-color-picker-color-block-inner"
style="background:rgb(22, 119, 255)"
style="background:rgb(22,119,255)"
/>
</div>
<div
@ -412,7 +499,7 @@ exports[`renders components/color-picker/demo/size.tsx correctly 1`] = `
>
<div
class="ant-color-picker-color-block-inner"
style="background:rgb(22, 119, 255)"
style="background:rgb(22,119,255)"
/>
</div>
<div
@ -433,7 +520,7 @@ exports[`renders components/color-picker/demo/size.tsx correctly 1`] = `
>
<div
class="ant-color-picker-color-block-inner"
style="background:rgb(22, 119, 255)"
style="background:rgb(22,119,255)"
/>
</div>
<div
@ -463,7 +550,7 @@ exports[`renders components/color-picker/demo/text-render.tsx correctly 1`] = `
>
<div
class="ant-color-picker-color-block-inner"
style="background:rgb(22, 119, 255)"
style="background:rgb(22,119,255)"
/>
</div>
<div
@ -484,7 +571,7 @@ exports[`renders components/color-picker/demo/text-render.tsx correctly 1`] = `
>
<div
class="ant-color-picker-color-block-inner"
style="background:rgb(22, 119, 255)"
style="background:rgb(22,119,255)"
/>
</div>
<div
@ -511,7 +598,7 @@ exports[`renders components/color-picker/demo/text-render.tsx correctly 1`] = `
>
<div
class="ant-color-picker-color-block-inner"
style="background:rgb(22, 119, 255)"
style="background:rgb(22,119,255)"
/>
</div>
<div
@ -564,7 +651,7 @@ exports[`renders components/color-picker/demo/trigger-event.tsx correctly 1`] =
>
<div
class="ant-color-picker-color-block-inner"
style="background:rgb(22, 119, 255)"
style="background:rgb(22,119,255)"
/>
</div>
</div>

View File

@ -59,7 +59,7 @@ exports[`ColorPicker Should panelRender work 1`] = `
style="position: relative;"
>
<div
style="position: absolute; left: -50px; top: 50px; z-index: 1;"
style="position: absolute; left: 0%; top: 100%; z-index: 1; transform: translate(-50%, -50%);"
>
<div
class="ant-color-picker-handler"
@ -79,46 +79,46 @@ exports[`ColorPicker Should panelRender work 1`] = `
class="ant-color-picker-slider-group"
>
<div
class="ant-color-picker-slider ant-color-picker-slider-hue"
class="ant-slider ant-color-picker-slider ant-slider-horizontal"
>
<div
class="ant-color-picker-palette"
style="position: relative;"
>
class="ant-slider-rail ant-color-picker-slider-rail"
/>
<div
style="position: absolute; left: -50px; top: -16.666666666666668px; z-index: 1;"
>
class="ant-slider-step"
/>
<div
class="ant-color-picker-handler ant-color-picker-handler-sm"
style="background-color: rgb(255, 0, 0);"
aria-disabled="false"
aria-orientation="horizontal"
aria-valuemax="359"
aria-valuemin="0"
aria-valuenow="0"
class="ant-slider-handle ant-slider-handle-1 ant-color-picker-slider-handle"
role="slider"
style="left: 0%; transform: translateX(-50%); background: rgb(255, 0, 0);"
tabindex="0"
/>
</div>
<div
class="ant-color-picker-gradient"
style="position: absolute; inset: 0;"
/>
</div>
</div>
<div
class="ant-color-picker-slider ant-color-picker-slider-alpha"
class="ant-slider ant-color-picker-slider ant-slider-horizontal"
>
<div
class="ant-color-picker-palette"
style="position: relative;"
>
<div
style="position: absolute; left: -50px; top: -16.666666666666668px; z-index: 1;"
>
<div
class="ant-color-picker-handler ant-color-picker-handler-sm"
style="background-color: rgba(0, 0, 0, 0);"
class="ant-slider-rail ant-color-picker-slider-rail"
/>
</div>
<div
class="ant-color-picker-gradient"
style="position: absolute; inset: 0;"
class="ant-slider-step"
/>
<div
aria-disabled="false"
aria-orientation="horizontal"
aria-valuemax="100"
aria-valuemin="0"
aria-valuenow="0"
class="ant-slider-handle ant-slider-handle-1 ant-color-picker-slider-handle"
role="slider"
style="left: 0%; transform: translateX(-50%); background: rgba(0, 0, 0, 0);"
tabindex="0"
/>
</div>
</div>
</div>
<div
@ -343,7 +343,7 @@ exports[`ColorPicker Should panelRender work 2`] = `
style="position: relative;"
>
<div
style="position: absolute; left: -50px; top: 50px; z-index: 1;"
style="position: absolute; left: 0%; top: 100%; z-index: 1; transform: translate(-50%, -50%);"
>
<div
class="ant-color-picker-handler"
@ -363,46 +363,46 @@ exports[`ColorPicker Should panelRender work 2`] = `
class="ant-color-picker-slider-group"
>
<div
class="ant-color-picker-slider ant-color-picker-slider-hue"
class="ant-slider ant-color-picker-slider ant-slider-horizontal"
>
<div
class="ant-color-picker-palette"
style="position: relative;"
>
class="ant-slider-rail ant-color-picker-slider-rail"
/>
<div
style="position: absolute; left: -50px; top: -16.666666666666668px; z-index: 1;"
>
class="ant-slider-step"
/>
<div
class="ant-color-picker-handler ant-color-picker-handler-sm"
style="background-color: rgb(255, 0, 0);"
aria-disabled="false"
aria-orientation="horizontal"
aria-valuemax="359"
aria-valuemin="0"
aria-valuenow="0"
class="ant-slider-handle ant-slider-handle-1 ant-color-picker-slider-handle"
role="slider"
style="left: 0%; transform: translateX(-50%); background: rgb(255, 0, 0);"
tabindex="0"
/>
</div>
<div
class="ant-color-picker-gradient"
style="position: absolute; inset: 0;"
/>
</div>
</div>
<div
class="ant-color-picker-slider ant-color-picker-slider-alpha"
class="ant-slider ant-color-picker-slider ant-slider-horizontal"
>
<div
class="ant-color-picker-palette"
style="position: relative;"
>
<div
style="position: absolute; left: -50px; top: -16.666666666666668px; z-index: 1;"
>
<div
class="ant-color-picker-handler ant-color-picker-handler-sm"
style="background-color: rgba(0, 0, 0, 0);"
class="ant-slider-rail ant-color-picker-slider-rail"
/>
</div>
<div
class="ant-color-picker-gradient"
style="position: absolute; inset: 0;"
class="ant-slider-step"
/>
<div
aria-disabled="false"
aria-orientation="horizontal"
aria-valuemax="100"
aria-valuemin="0"
aria-valuenow="0"
class="ant-slider-handle ant-slider-handle-1 ant-color-picker-slider-handle"
role="slider"
style="left: 0%; transform: translateX(-50%); background: rgba(0, 0, 0, 0);"
tabindex="0"
/>
</div>
</div>
</div>
<div

View File

@ -1,3 +0,0 @@
import demoTest from '../../../tests/shared/demoTest';
demoTest('color-picker');

View File

@ -0,0 +1,9 @@
import * as React from 'react';
import demoTest, { rootPropsTest } from '../../../tests/shared/demoTest';
demoTest('color-picker', {
testRootProps: false,
});
rootPropsTest('color-picker', (ColorPicker, props) => <ColorPicker {...props} value={undefined} />);

View File

@ -0,0 +1,309 @@
import React from 'react';
import { render } from '@testing-library/react';
import { spyElementPrototypes } from 'rc-util/lib/test/domHook';
import { resetWarned } from '../../_util/warning';
import { createEvent, fireEvent } from '../../../tests/utils';
import { AggregationColor } from '../color';
import ColorPicker from '../ColorPicker';
describe('ColorPicker.gradient', () => {
const errorSpy = jest.spyOn(console, 'error').mockImplementation(() => {});
beforeAll(() => {
spyElementPrototypes(HTMLElement, {
getBoundingClientRect: () => ({
width: 100,
height: 100,
left: 0,
top: 0,
bottom: 100,
right: 100,
}),
});
});
beforeEach(() => {
resetWarned();
jest.useFakeTimers();
});
afterEach(() => {
errorSpy.mockReset();
jest.useRealTimers();
});
function doMouseDown(
container: HTMLElement,
start: number,
query: string | HTMLElement = '.ant-slider-handle',
skipEventCheck = false,
) {
const ele = typeof query === 'object' ? query : container.querySelector(query)!;
const mouseDown = createEvent.mouseDown(ele);
(mouseDown as any).pageX = start;
(mouseDown as any).pageY = start;
const preventDefault = jest.fn();
Object.defineProperties(mouseDown, {
clientX: { get: () => start },
clientY: { get: () => start },
preventDefault: { value: preventDefault },
});
fireEvent.mouseEnter(ele);
fireEvent(ele, mouseDown);
// Should not prevent default since focus will not change
if (!skipEventCheck) {
expect(preventDefault).not.toHaveBeenCalled();
}
fireEvent.focus(ele);
}
function doMouseMove(end: number) {
const mouseMove = createEvent.mouseMove(document);
(mouseMove as any).pageX = end;
(mouseMove as any).pageY = end;
fireEvent(document, mouseMove);
}
function doDrag(
container: HTMLElement,
start: number,
end: number,
query: string | HTMLElement = '.ant-slider-handle',
skipEventCheck = false,
) {
doMouseDown(container, start, query, skipEventCheck);
// Drag
doMouseMove(end);
// Up
fireEvent.mouseUp(typeof query === 'object' ? query : container.querySelector(query)!);
}
it('switch', async () => {
const onChange = jest.fn();
const { container } = render(
<ColorPicker mode={['single', 'gradient']} defaultValue="#123456" open onChange={onChange} />,
);
// Switch to gradient
fireEvent.click(container.querySelectorAll(`.ant-segmented-item-input`)[1]);
expect(onChange).toHaveBeenCalledWith(
expect.anything(),
'linear-gradient(90deg, rgb(18,52,86) 0%, rgb(18,52,86) 100%)',
);
});
it('change color position', async () => {
const onChange = jest.fn();
const { container } = render(
<ColorPicker
mode={['single', 'gradient']}
defaultValue={[
{
color: '#FF0000',
percent: 0,
},
{
color: '#0000FF',
percent: 100,
},
]}
open
onChange={onChange}
/>,
);
// Move
doDrag(container, 0, 80);
expect(onChange).toHaveBeenCalledWith(
expect.anything(),
'linear-gradient(90deg, rgb(255,0,0) 80%, rgb(0,0,255) 100%)',
);
});
it('change color hex', async () => {
const onChange = jest.fn();
const { container } = render(
<ColorPicker
mode={['single', 'gradient']}
defaultValue={[
{
color: '#FF0000',
percent: 0,
},
{
color: '#0000FF',
percent: 100,
},
]}
open
onChange={onChange}
/>,
);
// Move
doDrag(
container,
0,
80,
container.querySelector<HTMLElement>(
'.ant-color-picker-slider-container .ant-slider-handle',
)!,
true,
);
expect(onChange).toHaveBeenCalledWith(
expect.anything(),
'linear-gradient(90deg, rgb(200,0,255) 0%, rgb(0,0,255) 100%)',
);
});
it('new color', async () => {
const onChange = jest.fn();
const { container } = render(
<ColorPicker
mode={['single', 'gradient']}
defaultValue={[
{
color: '#FF0000',
percent: 0,
},
{
color: '#0000FF',
percent: 100,
},
]}
open
onChange={onChange}
/>,
);
// Move
doDrag(container, 20, 30, '.ant-slider', true);
expect(onChange).toHaveBeenCalledWith(
expect.anything(),
'linear-gradient(90deg, rgb(255,0,0) 0%, rgb(204,0,51) 20%, rgb(0,0,255) 100%)',
);
expect(onChange).toHaveBeenCalledWith(
expect.anything(),
'linear-gradient(90deg, rgb(255,0,0) 0%, rgb(204,0,51) 30%, rgb(0,0,255) 100%)',
);
});
it('remove color', async () => {
const onChange = jest.fn();
const { container } = render(
<ColorPicker
mode={['single', 'gradient']}
defaultValue={[
{
color: '#FF0000',
percent: 0,
},
{
color: '#00FF00',
percent: 50,
},
{
color: '#000FF0',
percent: 80,
},
{
color: '#0000FF',
percent: 100,
},
]}
open
onChange={onChange}
/>,
);
// Delete remove first
fireEvent.keyDown(container.querySelector<HTMLElement>('.ant-slider-handle-1')!, {
key: 'Delete',
});
expect(onChange).toHaveBeenCalledWith(
expect.anything(),
'linear-gradient(90deg, rgb(0,255,0) 50%, rgb(0,15,240) 80%, rgb(0,0,255) 100%)',
);
// Drag remove last
onChange.mockReset();
doDrag(
container,
0,
9999999,
container.querySelector<HTMLElement>('.ant-slider-handle-3')!,
true,
);
expect(onChange).toHaveBeenCalledWith(
expect.anything(),
'linear-gradient(90deg, rgb(0,255,0) 50%, rgb(0,15,240) 80%)',
);
});
it('invalid not crash', async () => {
render(<ColorPicker mode={['single', 'gradient']} defaultValue={[]} open />);
});
it('change to single', async () => {
const onChange = jest.fn();
const { container } = render(
<ColorPicker
mode={['single', 'gradient']}
defaultValue={[
{
color: '#FF0000',
percent: 0,
},
{
color: '#0000FF',
percent: 100,
},
]}
open
onChange={onChange}
/>,
);
// Switch to gradient
fireEvent.click(container.querySelector(`.ant-segmented-item-input`)!);
expect(onChange).toHaveBeenCalledWith(expect.anything(), 'rgb(255,0,0)');
});
it('not crash when pass gradient color', async () => {
const color = new AggregationColor([
{
color: '#FF0000',
percent: 0,
},
]);
const newColor = new AggregationColor(color);
expect(newColor.toCssString()).toEqual('linear-gradient(90deg, rgb(255,0,0) 0%)');
});
it('mode fallback', () => {
const { container } = render(<ColorPicker mode={['gradient']} defaultValue="#F00" open />);
expect(container.querySelector('.ant-color-picker-gradient-slider')).toBeTruthy();
});
});

View File

@ -10,7 +10,7 @@ import Button from '../../button';
import ConfigProvider from '../../config-provider';
import Form from '../../form';
import theme from '../../theme';
import type { Color } from '../color';
import { AggregationColor } from '../color';
import ColorPicker from '../ColorPicker';
import type { ColorPickerProps, ColorValueType } from '../interface';
import { generateColor } from '../util';
@ -19,13 +19,21 @@ function doMouseMove(
container: HTMLElement,
start: number,
end: number,
element = 'ant-color-picker-handler',
element: string | HTMLElement = 'ant-color-picker-handler',
) {
const mouseDown = createEvent.mouseDown(container.getElementsByClassName(element)[0], {
const ele =
element instanceof HTMLElement ? element : container.getElementsByClassName(element)[0];
const mouseDown = createEvent.mouseDown(ele, {
pageX: start,
pageY: start,
});
fireEvent(container.getElementsByClassName(element)[0], mouseDown);
Object.defineProperties(mouseDown, {
pageX: { get: () => start },
pageY: { get: () => start },
});
fireEvent(ele, mouseDown);
// Drag
const mouseMove: any = new Event('mousemove');
mouseMove.pageX = end;
@ -65,7 +73,7 @@ describe('ColorPicker', () => {
it('Should component custom trigger work', async () => {
const App: React.FC = () => {
const [color, setColor] = useState<Color | string>('hsb(215, 91%, 100%)');
const [color, setColor] = useState<AggregationColor | string>('hsb(215, 91%, 100%)');
const colorString = useMemo(
() => (typeof color === 'string' ? color : color.toHsbString()),
[color],
@ -340,7 +348,7 @@ describe('ColorPicker', () => {
});
it('Should fix hover boundary issues', async () => {
spyElementPrototypes(HTMLElement, {
const spyRect = spyElementPrototypes(HTMLElement, {
getBoundingClientRect: () => ({
x: 0,
y: 100,
@ -356,6 +364,8 @@ describe('ColorPicker', () => {
fireEvent.mouseLeave(container.querySelector('.ant-color-picker-trigger')!);
await waitFakeTimer();
expect(container.querySelector('.ant-popover-hidden')).toBeTruthy();
spyRect.mockRestore();
});
it('Should work at dark mode', async () => {
@ -385,6 +395,12 @@ describe('ColorPicker', () => {
expect(targetEle?.innerHTML).toBe('#1677ff');
});
it('showText with transparent', async () => {
const { container } = render(<ColorPicker defaultValue={null} showText />);
const targetEle = container.querySelector('.ant-color-picker-trigger-text');
expect(targetEle?.textContent).toBe('Transparent');
});
it('Should showText work', async () => {
const { container } = render(<ColorPicker defaultValue="#1677ff" open showText />);
const targetEle = container.querySelector('.ant-color-picker-trigger-text');
@ -404,7 +420,7 @@ describe('ColorPicker', () => {
await waitFakeTimer();
fireEvent.click(container.querySelector('.ant-select-item[title="RGB"]')!);
await waitFakeTimer();
expect(targetEle?.innerHTML).toEqual('rgb(22, 119, 255)');
expect(targetEle?.innerHTML).toEqual('rgb(22,119,255)');
fireEvent.mouseDown(
container.querySelector('.ant-color-picker-format-select .ant-select-selector')!,
@ -448,7 +464,7 @@ describe('ColorPicker', () => {
});
it('Should null work as expect', async () => {
spyElementPrototypes(HTMLElement, {
const spyRect = spyElementPrototypes(HTMLElement, {
getBoundingClientRect: () => ({
x: 0,
y: 100,
@ -456,7 +472,8 @@ describe('ColorPicker', () => {
height: 100,
}),
});
const { container } = render(<ColorPicker value={null} open />);
const { container } = render(<ColorPicker defaultValue={null} open />);
expect(
container.querySelector('.ant-color-picker-alpha-input input')?.getAttribute('value'),
).toEqual('0%');
@ -467,6 +484,8 @@ describe('ColorPicker', () => {
expect(
container.querySelector('.ant-color-picker-alpha-input input')?.getAttribute('value'),
).toEqual('100%');
spyRect.mockRestore();
});
it('should support valid in form', async () => {
@ -501,17 +520,37 @@ describe('ColorPicker', () => {
});
it('Should onChangeComplete work', async () => {
const spyRect = spyElementPrototypes(HTMLElement, {
getBoundingClientRect: () => ({
x: 0,
y: 100,
width: 100,
height: 100,
}),
});
const handleChangeComplete = jest.fn();
const { container } = render(
<ColorPicker open onChangeComplete={handleChangeComplete} allowClear />,
);
// Move
doMouseMove(container, 0, 999);
fireEvent.click(container.querySelector('.ant-color-picker-clear')!);
expect(handleChangeComplete).toHaveBeenCalledTimes(1);
// Clear
fireEvent.click(
container.querySelector('.ant-color-picker-operation .ant-color-picker-clear')!,
);
expect(handleChangeComplete).toHaveBeenCalledTimes(2);
// Change
fireEvent.change(container.querySelector('.ant-color-picker-hex-input input')!, {
target: { value: '#273B57' },
});
expect(handleChangeComplete).toHaveBeenCalledTimes(3);
spyRect.mockRestore();
});
it('Should disabledAlpha work', async () => {
@ -522,7 +561,7 @@ describe('ColorPicker', () => {
});
it('Should disabledAlpha work with value', async () => {
spyElementPrototypes(HTMLElement, {
const spyRect = spyElementPrototypes(HTMLElement, {
getBoundingClientRect: () => ({
x: 0,
y: 100,
@ -542,10 +581,12 @@ describe('ColorPicker', () => {
onChangeComplete={setChangedValue}
>
<div className="color-value">
{typeof value === 'string' ? value : value?.toHexString()}
{value instanceof AggregationColor ? value.toHexString() : String(value)}
</div>
<div className="color-value-changed">
{typeof changedValue === 'string' ? changedValue : changedValue?.toHexString()}
{changedValue instanceof AggregationColor
? changedValue.toHexString()
: String(changedValue)}
</div>
</ColorPicker>
);
@ -555,6 +596,8 @@ describe('ColorPicker', () => {
doMouseMove(container, 0, 999);
expect(container.querySelector('.color-value')?.innerHTML).toEqual('#000000');
expect(container.querySelector('.color-value-changed')?.innerHTML).toEqual('#000000');
spyRect.mockRestore();
});
it('Should warning work when set disabledAlpha true and color is alpha color', () => {
@ -616,7 +659,7 @@ describe('ColorPicker', () => {
const Demo = () => {
const [color, setColor] = useState<ColorValueType>(value);
useEffect(() => {
setColor(generateColor('red'));
setColor(generateColor('#FF0000'));
}, []);
return <ColorPicker value={color} />;
};
@ -645,7 +688,7 @@ describe('ColorPicker', () => {
it('Controlled string value should work with allowClear correctly', async () => {
const Demo = (props: any) => {
const [color, setColor] = useState<ColorValueType>(generateColor('red'));
const [color, setColor] = useState<ColorValueType>(generateColor('#FF0000'));
useEffect(() => {
if (typeof props.value !== 'undefined') {
@ -654,7 +697,14 @@ describe('ColorPicker', () => {
}, [props.value]);
return (
<ColorPicker value={color} onChange={(e) => setColor(e.toHexString())} open allowClear />
<ColorPicker
value={color}
onChange={(e) => {
setColor(e.toHexString());
}}
open
allowClear
/>
);
};
const { container, rerender } = render(<Demo />);
@ -662,10 +712,13 @@ describe('ColorPicker', () => {
expect(
container.querySelector('.ant-color-picker-trigger .ant-color-picker-clear'),
).toBeFalsy();
// Clear
fireEvent.click(container.querySelector('.ant-color-picker-clear')!);
expect(
container.querySelector('.ant-color-picker-trigger .ant-color-picker-clear'),
).toBeTruthy();
rerender(<Demo value="#1677ff" />);
expect(
container.querySelector('.ant-color-picker-trigger .ant-color-picker-clear'),
@ -740,4 +793,59 @@ describe('ColorPicker', () => {
expect(container.querySelector('.ant-color-picker-clear')).toBeFalsy();
});
});
it('toHex', async () => {
const { container } = render(
<ColorPicker defaultValue="#123456" showText={(color) => color.toHex()} />,
);
expect(container.querySelector('.ant-color-picker-trigger-text')?.innerHTML).toBe('123456');
});
describe('transparent to valuable', () => {
let spyRect: ReturnType<typeof spyElementPrototypes>;
beforeEach(() => {
spyRect = spyElementPrototypes(HTMLElement, {
getBoundingClientRect: () => ({
x: 0,
y: 100,
width: 100,
height: 100,
}),
});
});
afterEach(() => {
spyRect.mockRestore();
});
it('init with hue', async () => {
const onChange = jest.fn();
const { container } = render(<ColorPicker defaultValue={null} open onChange={onChange} />);
doMouseMove(container, 0, 50, 'ant-color-picker-slider-handle');
expect(onChange).toHaveBeenCalledWith(
expect.anything(),
// Safe to change with any value but (0/0/0/0)
'rgb(0,255,255)',
);
});
it('init with alpha', async () => {
const onChange = jest.fn();
const { container } = render(<ColorPicker defaultValue={null} open onChange={onChange} />);
doMouseMove(
container,
0,
50,
container.querySelectorAll<HTMLElement>('.ant-color-picker-slider-handle')[1]!,
);
expect(onChange).toHaveBeenCalledWith(
expect.anything(),
// Safe to change with any value but (0/0/0/0)
'rgba(255,0,0,0.5)',
);
});
});
});

View File

@ -1,30 +1,52 @@
/* eslint-disable class-methods-use-this */
import type { ColorGenInput } from '@rc-component/color-picker';
import { Color as RcColor } from '@rc-component/color-picker';
import type { ColorGenInput, Colors } from './interface';
export const toHexFormat = (value?: string, alpha?: boolean) =>
value?.replace(/[^\w/]/gi, '').slice(0, alpha ? 8 : 6) || '';
export const getHex = (value?: string, alpha?: boolean) => (value ? toHexFormat(value, alpha) : '');
export interface Color
extends Pick<
RcColor,
'toHsb' | 'toHsbString' | 'toHex' | 'toHexString' | 'toRgb' | 'toRgbString'
> {
cleared: boolean | 'controlled';
}
export type GradientColor = {
color: AggregationColor;
percent: number;
}[];
export class ColorFactory implements Color {
export class AggregationColor {
/** Original Color object */
private metaColor: RcColor;
private colors: GradientColor | undefined;
public cleared = false;
constructor(color: ColorGenInput<Color>) {
this.metaColor = new RcColor(color as ColorGenInput);
if (!color) {
this.metaColor.setAlpha(0);
constructor(color: ColorGenInput<AggregationColor> | Colors<AggregationColor>) {
// Clone from another AggregationColor
if (color instanceof AggregationColor) {
this.metaColor = color.metaColor.clone();
this.colors = color.colors?.map((info) => ({
color: new AggregationColor(info.color),
percent: info.percent,
}));
this.cleared = color.cleared;
return;
}
const isArray = Array.isArray(color);
if (isArray && color.length) {
this.colors = color.map(({ color: c, percent }) => ({
color: new AggregationColor(c),
percent,
}));
this.metaColor = new RcColor(this.colors[0].color.metaColor);
} else {
this.metaColor = new RcColor(isArray ? '' : color);
}
if (!color || (isArray && !this.colors)) {
this.metaColor = this.metaColor.setA(0);
this.cleared = true;
}
}
@ -38,13 +60,11 @@ export class ColorFactory implements Color {
}
toHex() {
return getHex(this.toHexString(), this.metaColor.getAlpha() < 1);
return getHex(this.toHexString(), this.metaColor.a < 1);
}
toHexString() {
return this.metaColor.getAlpha() === 1
? this.metaColor.toHexString()
: this.metaColor.toHex8String();
return this.metaColor.toHexString();
}
toRgb() {
@ -54,4 +74,42 @@ export class ColorFactory implements Color {
toRgbString() {
return this.metaColor.toRgbString();
}
isGradient(): boolean {
return !!this.colors && !this.cleared;
}
getColors(): GradientColor {
return this.colors || [{ color: this, percent: 0 }];
}
toCssString(): string {
const { colors } = this;
// CSS line-gradient
if (colors) {
const colorsStr = colors.map((c) => `${c.color.toRgbString()} ${c.percent}%`).join(', ');
return `linear-gradient(90deg, ${colorsStr})`;
}
return this.metaColor.toRgbString();
}
equals(color: AggregationColor | null): boolean {
if (!color || this.isGradient() !== color.isGradient()) {
return false;
}
if (!this.isGradient()) {
return this.toHexString() === color.toHexString();
}
return (
this.colors!.length === color.colors!.length &&
this.colors!.every((c, i) => {
const target = color.colors![i];
return c.percent === target.percent && c.color.equals(target.color);
})
);
}
}

View File

@ -1,19 +1,19 @@
import type { FC } from 'react';
import React, { useEffect, useState } from 'react';
import type { Color } from '../color';
import type { ColorPickerBaseProps } from '../interface';
import { generateColor, getAlphaColor } from '../util';
import type { AggregationColor } from '../color';
import { generateColor, getColorAlpha } from '../util';
import ColorSteppers from './ColorSteppers';
interface ColorAlphaInputProps extends Pick<ColorPickerBaseProps, 'prefixCls'> {
value?: Color;
onChange?: (value: Color) => void;
interface ColorAlphaInputProps {
prefixCls: string;
value?: AggregationColor;
onChange?: (value: AggregationColor) => void;
}
const ColorAlphaInput: FC<ColorAlphaInputProps> = ({ prefixCls, value, onChange }) => {
const colorAlphaInputPrefixCls = `${prefixCls}-alpha-input`;
const [alphaValue, setAlphaValue] = useState<Color>(generateColor(value || '#000'));
const [alphaValue, setAlphaValue] = useState<AggregationColor>(generateColor(value || '#000'));
// Update step value
useEffect(() => {
@ -34,7 +34,7 @@ const ColorAlphaInput: FC<ColorAlphaInputProps> = ({ prefixCls, value, onChange
return (
<ColorSteppers
value={getAlphaColor(alphaValue)}
value={getColorAlpha(alphaValue)}
prefixCls={prefixCls}
formatter={(step) => `${step}%`}
className={colorAlphaInputPrefixCls}

View File

@ -1,23 +1,24 @@
import type { FC } from 'react';
import React from 'react';
import type { Color } from '../color';
import type { ColorPickerBaseProps } from '../interface';
import type { AggregationColor } from '../color';
import { generateColor } from '../util';
interface ColorClearProps extends Pick<ColorPickerBaseProps, 'prefixCls'> {
value?: Color;
onChange?: (value: Color) => void;
interface ColorClearProps {
prefixCls: string;
value?: AggregationColor;
onChange?: (value: AggregationColor) => void;
}
const ColorClear: FC<ColorClearProps> = ({ prefixCls, value, onChange }) => {
const handleClick = () => {
if (value && !value.cleared) {
if (onChange && value && !value.cleared) {
const hsba = value.toHsb();
hsba.a = 0;
const genColor = generateColor(hsba);
genColor.cleared = true;
onChange?.(genColor);
onChange(genColor);
}
};
return <div className={`${prefixCls}-clear`} onClick={handleClick} />;

View File

@ -2,14 +2,14 @@ import type { FC } from 'react';
import React, { useEffect, useState } from 'react';
import Input from '../../input';
import type { Color } from '../color';
import type { AggregationColor } from '../color';
import { toHexFormat } from '../color';
import type { ColorPickerBaseProps } from '../interface';
import { generateColor } from '../util';
interface ColorHexInputProps extends Pick<ColorPickerBaseProps, 'prefixCls'> {
value?: Color;
onChange?: (value: Color) => void;
interface ColorHexInputProps {
prefixCls: string;
value?: AggregationColor;
onChange?: (value: AggregationColor) => void;
}
const hexReg = /(^#[\da-f]{6}$)|(^#[\da-f]{8}$)/i;
@ -17,13 +17,14 @@ const isHexString = (hex?: string) => hexReg.test(`#${hex}`);
const ColorHexInput: FC<ColorHexInputProps> = ({ prefixCls, value, onChange }) => {
const colorHexInputPrefixCls = `${prefixCls}-hex-input`;
const [hexValue, setHexValue] = useState(value?.toHex());
const [hexValue, setHexValue] = useState(() =>
value ? toHexFormat(value.toHexString()) : undefined,
);
// Update step value
useEffect(() => {
const hex = value?.toHex();
if (isHexString(hex) && value) {
setHexValue(toHexFormat(hex));
if (value) {
setHexValue(toHexFormat(value.toHexString()));
}
}, [value]);

View File

@ -2,19 +2,19 @@ import type { FC } from 'react';
import React, { useEffect, useState } from 'react';
import type { HSB } from '@rc-component/color-picker';
import type { Color } from '../color';
import type { ColorPickerBaseProps } from '../interface';
import type { AggregationColor } from '../color';
import { generateColor, getRoundNumber } from '../util';
import ColorSteppers from './ColorSteppers';
interface ColorHsbInputProps extends Pick<ColorPickerBaseProps, 'prefixCls'> {
value?: Color;
onChange?: (value: Color) => void;
interface ColorHsbInputProps {
prefixCls: string;
value?: AggregationColor;
onChange?: (value: AggregationColor) => void;
}
const ColorHsbInput: FC<ColorHsbInputProps> = ({ prefixCls, value, onChange }) => {
const colorHsbInputPrefixCls = `${prefixCls}-hsb-input`;
const [hsbValue, setHsbValue] = useState<Color>(generateColor(value || '#000'));
const [hsbValue, setHsbValue] = useState<AggregationColor>(generateColor(value || '#000'));
// Update step value
useEffect(() => {

View File

@ -3,18 +3,21 @@ import React, { useMemo } from 'react';
import useMergedState from 'rc-util/lib/hooks/useMergedState';
import Select from '../../select';
import type { Color } from '../color';
import type { ColorFormatType, ColorPickerBaseProps } from '../interface';
import type { AggregationColor } from '../color';
import type { ColorFormatType } from '../interface';
import { ColorFormat } from '../interface';
import ColorAlphaInput from './ColorAlphaInput';
import ColorHexInput from './ColorHexInput';
import ColorHsbInput from './ColorHsbInput';
import ColorRgbInput from './ColorRgbInput';
interface ColorInputProps
extends Pick<ColorPickerBaseProps, 'prefixCls' | 'format' | 'onFormatChange' | 'disabledAlpha'> {
value?: Color;
onChange?: (value: Color) => void;
interface ColorInputProps {
prefixCls: string;
format?: ColorFormatType;
onFormatChange?: (format: ColorFormatType) => void;
disabledAlpha?: boolean;
value?: AggregationColor;
onChange?: (value: AggregationColor) => void;
}
const selectOptions = [ColorFormat.hex, ColorFormat.hsb, ColorFormat.rgb].map((format) => ({

View File

@ -8,14 +8,15 @@ import type { CollapseProps } from '../../collapse';
import Collapse from '../../collapse';
import { useLocale } from '../../locale';
import { useToken } from '../../theme/internal';
import type { Color } from '../color';
import type { ColorPickerBaseProps, PresetsItem } from '../interface';
import type { AggregationColor } from '../color';
import type { PresetsItem } from '../interface';
import { generateColor } from '../util';
interface ColorPresetsProps extends Pick<ColorPickerBaseProps, 'prefixCls'> {
interface ColorPresetsProps {
prefixCls: string;
presets: PresetsItem[];
value?: Color;
onChange?: (value: Color) => void;
value?: AggregationColor;
onChange?: (value: AggregationColor) => void;
}
const genPresetColor = (list: PresetsItem[]) =>
@ -24,7 +25,7 @@ const genPresetColor = (list: PresetsItem[]) =>
return value;
});
const isBright = (value: Color, bgColorToken: string) => {
const isBright = (value: AggregationColor, bgColorToken: string) => {
const { r, g, b, a } = value.toRgb();
const hsv = new RcColor(value.toRgbString()).onBackground(bgColorToken).toHsv();
if (a <= 0.5) {
@ -55,7 +56,7 @@ const ColorPresets: FC<ColorPresetsProps> = ({ prefixCls, presets, value: color,
[presetsValue],
);
const handleClick = (colorValue: Color) => {
const handleClick = (colorValue: AggregationColor) => {
onChange?.(colorValue);
};
@ -65,7 +66,7 @@ const ColorPresets: FC<ColorPresetsProps> = ({ prefixCls, presets, value: color,
children: (
<div className={`${colorPresetsPrefixCls}-items`}>
{Array.isArray(preset?.colors) && preset.colors?.length > 0 ? (
(preset.colors as Color[]).map((presetColor, index) => (
(preset.colors as AggregationColor[]).map((presetColor, index) => (
<ColorBlock
// eslint-disable-next-line react/no-array-index-key
key={`preset-${index}-${presetColor.toHexString()}`}

View File

@ -2,19 +2,19 @@ import type { FC } from 'react';
import React, { useEffect, useState } from 'react';
import type { RGB } from '@rc-component/color-picker';
import type { Color } from '../color';
import type { ColorPickerBaseProps } from '../interface';
import type { AggregationColor } from '../color';
import { generateColor } from '../util';
import ColorSteppers from './ColorSteppers';
interface ColorRgbInputProps extends Pick<ColorPickerBaseProps, 'prefixCls'> {
value?: Color;
onChange?: (value: Color) => void;
interface ColorRgbInputProps {
prefixCls: string;
value?: AggregationColor;
onChange?: (value: AggregationColor) => void;
}
const ColorRgbInput: FC<ColorRgbInputProps> = ({ prefixCls, value, onChange }) => {
const colorRgbInputPrefixCls = `${prefixCls}-rgb-input`;
const [rgbValue, setRgbValue] = useState<Color>(generateColor(value || '#000'));
const [rgbValue, setRgbValue] = useState<AggregationColor>(generateColor(value || '#000'));
// Update step value
useEffect(() => {

View File

@ -0,0 +1,177 @@
import * as React from 'react';
import type { BaseSliderProps } from '@rc-component/color-picker';
import classNames from 'classnames';
import { UnstableContext } from 'rc-slider';
import { useEvent } from 'rc-util';
import type { GetContextProp, GetProp } from '../../_util/type';
import Slider from '../../slider';
import SliderInternalContext from '../../slider/Context';
import type { SliderInternalContextProps } from '../../slider/Context';
import { getGradientPercentColor } from '../util';
export interface GradientColorSliderProps
extends Omit<BaseSliderProps, 'value' | 'onChange' | 'onChangeComplete' | 'type'> {
value: number[];
onChange?: (value: number[]) => void;
onChangeComplete: (value: number[]) => void;
range?: boolean;
className?: string;
activeIndex?: number;
onActive?: (index: number) => void;
type: BaseSliderProps['type'] | 'gradient';
// Drag events
onDragStart?: GetContextProp<typeof UnstableContext, 'onDragStart'>;
onDragChange?: GetContextProp<typeof UnstableContext, 'onDragChange'>;
// Key event
onKeyDelete?: (index: number) => void;
}
export const GradientColorSlider = (props: GradientColorSliderProps) => {
const {
prefixCls,
colors,
type,
color,
range = false,
className,
activeIndex,
onActive,
onDragStart,
onDragChange,
onKeyDelete,
...restProps
} = props;
const sliderProps = {
...restProps,
track: false,
};
// ========================== Background ==========================
const linearCss = React.useMemo(() => {
const colorsStr = colors.map((c) => `${c.color} ${c.percent}%`).join(', ');
return `linear-gradient(90deg, ${colorsStr})`;
}, [colors]);
const pointColor = React.useMemo(() => {
if (!color || !type) {
return null;
}
if (type === 'alpha') {
return color.toRgbString();
}
return `hsl(${color.toHsb().h}, 100%, 50%)`;
}, [color, type]);
// ======================= Context: Slider ========================
const onInternalDragStart: GetContextProp<typeof UnstableContext, 'onDragStart'> = useEvent(
onDragStart!,
);
const onInternalDragChange: GetContextProp<typeof UnstableContext, 'onDragChange'> = useEvent(
onDragChange!,
);
const unstableContext = React.useMemo(
() => ({
onDragStart: onInternalDragStart,
onDragChange: onInternalDragChange,
}),
[],
);
// ======================= Context: Render ========================
const handleRender: GetProp<SliderInternalContextProps, 'handleRender'> = useEvent(
(ori, info) => {
const { onFocus, style, className: handleCls, onKeyDown } = ori.props;
// Point Color
const mergedStyle = { ...style };
if (type === 'gradient') {
mergedStyle.background = getGradientPercentColor(colors, info.value);
}
return React.cloneElement(ori, {
onFocus: (e: React.FocusEvent<HTMLDivElement>) => {
onActive?.(info.index);
onFocus?.(e);
},
style: mergedStyle,
className: classNames(handleCls, {
[`${prefixCls}-slider-handle-active`]: activeIndex === info.index,
}),
onKeyDown: (e: React.KeyboardEvent<HTMLDivElement>) => {
if ((e.key === 'Delete' || e.key === 'Backspace') && onKeyDelete) {
onKeyDelete(info.index);
}
onKeyDown?.(e);
},
});
},
);
const sliderContext: SliderInternalContextProps = React.useMemo(
() => ({
direction: 'ltr',
handleRender,
}),
[],
);
// ============================ Render ============================
return (
<SliderInternalContext.Provider value={sliderContext}>
<UnstableContext.Provider value={unstableContext}>
<Slider
{...sliderProps}
className={classNames(className, `${prefixCls}-slider`)}
tooltip={{ open: false }}
range={{
editable: range,
minCount: 2,
}}
styles={{
rail: {
background: linearCss,
},
handle: pointColor
? {
background: pointColor,
}
: {},
}}
classNames={{
rail: `${prefixCls}-slider-rail`,
handle: `${prefixCls}-slider-handle`,
}}
/>
</UnstableContext.Provider>
</SliderInternalContext.Provider>
);
};
const SingleColorSlider = (props: BaseSliderProps) => {
const { value, onChange, onChangeComplete } = props;
const singleOnChange = (v: number[]) => onChange(v[0]);
const singleOnChangeComplete = (v: number[]) => onChangeComplete(v[0]);
return (
<GradientColorSlider
{...props}
value={[value]}
onChange={singleOnChange}
onChangeComplete={singleOnChangeComplete}
/>
);
};
export default SingleColorSlider;

View File

@ -4,9 +4,9 @@ import classNames from 'classnames';
import type { InputNumberProps } from '../../input-number';
import InputNumber from '../../input-number';
import type { ColorPickerBaseProps } from '../interface';
interface ColorSteppersProps extends Pick<ColorPickerBaseProps, 'prefixCls'> {
interface ColorSteppersProps {
prefixCls: string;
value?: number;
min?: number;
max?: number;

View File

@ -1,15 +1,21 @@
/* eslint-disable react/no-array-index-key */
import type { CSSProperties, MouseEventHandler } from 'react';
import React, { forwardRef, useMemo } from 'react';
import { ColorBlock } from '@rc-component/color-picker';
import classNames from 'classnames';
import pickAttrs from 'rc-util/lib/pickAttrs';
import type { ColorPickerBaseProps, ColorPickerProps } from '../interface';
import { getAlphaColor } from '../util';
import { useLocale } from '../../locale';
import type { AggregationColor } from '../color';
import type { ColorFormatType, ColorPickerProps } from '../interface';
import { getColorAlpha } from '../util';
import ColorClear from './ColorClear';
export interface ColorTriggerProps
extends Pick<ColorPickerBaseProps, 'prefixCls' | 'disabled' | 'format'> {
color: NonNullable<ColorPickerBaseProps['color']>;
export interface ColorTriggerProps {
prefixCls: string;
disabled?: boolean;
format?: ColorFormatType;
color: AggregationColor;
open?: boolean;
showText?: ColorPickerProps['showText'];
className?: string;
@ -17,25 +23,57 @@ export interface ColorTriggerProps
onClick?: MouseEventHandler<HTMLDivElement>;
onMouseEnter?: MouseEventHandler<HTMLDivElement>;
onMouseLeave?: MouseEventHandler<HTMLDivElement>;
activeIndex: number;
}
const ColorTrigger = forwardRef<HTMLDivElement, ColorTriggerProps>((props, ref) => {
const { color, prefixCls, open, disabled, format, className, showText, ...rest } = props;
const { color, prefixCls, open, disabled, format, className, showText, activeIndex, ...rest } =
props;
const colorTriggerPrefixCls = `${prefixCls}-trigger`;
const colorTextPrefixCls = `${colorTriggerPrefixCls}-text`;
const colorTextCellPrefixCls = `${colorTextPrefixCls}-cell`;
const containerNode = useMemo<React.ReactNode>(
() =>
color.cleared ? (
<ColorClear prefixCls={prefixCls} />
) : (
<ColorBlock prefixCls={prefixCls} color={color.toRgbString()} />
),
[color, prefixCls],
const [locale] = useLocale('ColorPicker');
// ============================== Text ==============================
const desc: React.ReactNode = React.useMemo(() => {
if (!showText) {
return '';
}
if (typeof showText === 'function') {
return showText(color);
}
if (color.cleared) {
return locale.transparent;
}
if (color.isGradient()) {
// return color
// .getColors()
// .map((c) => `${c.color.toRgbString()} ${c.percent}%`)
// .join(', ');
return color.getColors().map((c, index) => {
const inactive = activeIndex !== -1 && activeIndex !== index;
return (
<span
key={index}
className={classNames(
colorTextCellPrefixCls,
inactive && `${colorTextCellPrefixCls}-inactive`,
)}
>
{c.color.toRgbString()} {c.percent}%
</span>
);
});
}
const genColorString = () => {
const hexString = color.toHexString().toUpperCase();
const alpha = getAlphaColor(color);
const alpha = getColorAlpha(color);
switch (format) {
case 'rgb':
return color.toRgbString();
@ -45,16 +83,18 @@ const ColorTrigger = forwardRef<HTMLDivElement, ColorTriggerProps>((props, ref)
default:
return alpha < 100 ? `${hexString.slice(0, 7)},${alpha}%` : hexString;
}
};
}, [color, format, showText, activeIndex]);
const renderText = () => {
if (typeof showText === 'function') {
return showText(color);
}
if (showText) {
return genColorString();
}
};
// ============================= Render =============================
const containerNode = useMemo<React.ReactNode>(
() =>
color.cleared ? (
<ColorClear prefixCls={prefixCls} />
) : (
<ColorBlock prefixCls={prefixCls} color={color.toCssString()} />
),
[color, prefixCls],
);
return (
<div
@ -63,10 +103,10 @@ const ColorTrigger = forwardRef<HTMLDivElement, ColorTriggerProps>((props, ref)
[`${colorTriggerPrefixCls}-active`]: open,
[`${colorTriggerPrefixCls}-disabled`]: disabled,
})}
{...rest}
{...pickAttrs(rest)}
>
{containerNode}
{showText && <div className={`${colorTriggerPrefixCls}-text`}>{renderText()}</div>}
{showText && <div className={colorTextPrefixCls}>{desc}</div>}
</div>
);
});

View File

@ -1,68 +0,0 @@
import type { FC } from 'react';
import React, { useContext } from 'react';
import type { HsbaColorType } from '@rc-component/color-picker';
import RcColorPicker from '@rc-component/color-picker';
import type { Color } from '../color';
import { PanelPickerContext } from '../context';
import type { ColorPickerBaseProps } from '../interface';
import { generateColor } from '../util';
import ColorClear from './ColorClear';
import ColorInput from './ColorInput';
export interface PanelPickerProps
extends Pick<
ColorPickerBaseProps,
'prefixCls' | 'allowClear' | 'disabledAlpha' | 'onChangeComplete'
> {
value?: Color;
onChange?: (value?: Color, type?: HsbaColorType, pickColor?: boolean) => void;
onClear?: () => void;
}
const PanelPicker: FC = () => {
const {
prefixCls,
allowClear,
value,
disabledAlpha,
onChange,
onClear,
onChangeComplete,
...injectProps
} = useContext(PanelPickerContext);
return (
<>
{allowClear && (
<ColorClear
prefixCls={prefixCls}
value={value}
onChange={(clearColor) => {
onChange?.(clearColor);
onClear?.();
}}
{...injectProps}
/>
)}
<RcColorPicker
prefixCls={prefixCls}
value={value?.toHsb()}
disabledAlpha={disabledAlpha}
onChange={(colorValue, type) => {
onChange?.(generateColor(colorValue), type, true);
}}
onChangeComplete={(colorValue) => {
onChangeComplete?.(generateColor(colorValue));
}}
/>
<ColorInput
value={value}
onChange={onChange}
prefixCls={prefixCls}
disabledAlpha={disabledAlpha}
{...injectProps}
/>
</>
);
};
export default PanelPicker;

View File

@ -0,0 +1,149 @@
import * as React from 'react';
import type { UnstableContext } from 'rc-slider';
import type { GetContextProp } from '../../../_util/type';
import { AggregationColor } from '../../color';
import type { GradientColor } from '../../color';
import type { PanelPickerContextProps } from '../../context';
import { getGradientPercentColor } from '../../util';
import { GradientColorSlider } from '../ColorSlider';
function sortColors(colors: { percent: number; color: string }[]) {
return [...colors].sort((a, b) => a.percent - b.percent);
}
export interface GradientColorBarProps extends PanelPickerContextProps {
colors: GradientColor;
}
/**
* GradientColorBar will auto show when the mode is `gradient`.
*/
const GradientColorBar = (props: GradientColorBarProps) => {
const {
prefixCls,
mode,
onChange,
onChangeComplete,
onActive,
activeIndex,
onGradientDragging,
colors,
} = props;
const isGradient = mode === 'gradient';
// ============================= Colors =============================
const colorList = React.useMemo(
() =>
colors.map((info) => ({
percent: info.percent,
color: info.color.toRgbString(),
})),
[colors],
);
const values = React.useMemo(() => colorList.map((info) => info.percent), [colorList]);
// ============================== Drag ==============================
const colorsRef = React.useRef(colorList);
// Record current colors
const onDragStart: GetContextProp<typeof UnstableContext, 'onDragStart'> = ({
rawValues,
draggingIndex,
draggingValue,
}) => {
if (rawValues.length > colorList.length) {
// Add new node
const newPointColor = getGradientPercentColor(colorList, draggingValue);
const nextColors = [...colorList];
nextColors.splice(draggingIndex, 0, {
percent: draggingValue,
color: newPointColor,
});
colorsRef.current = nextColors;
} else {
colorsRef.current = colorList;
}
onGradientDragging(true);
onChange(new AggregationColor(sortColors(colorsRef.current)), true);
};
// Adjust color when dragging
const onDragChange: GetContextProp<typeof UnstableContext, 'onDragChange'> = ({
deleteIndex,
draggingIndex,
draggingValue,
}) => {
let nextColors = [...colorsRef.current];
if (deleteIndex !== -1) {
nextColors.splice(deleteIndex, 1);
} else {
nextColors[draggingIndex] = {
...nextColors[draggingIndex],
percent: draggingValue,
};
nextColors = sortColors(nextColors);
}
onChange(new AggregationColor(nextColors), true);
};
// ============================== Key ===============================
const onKeyDelete = (index: number) => {
const nextColors = [...colorList];
nextColors.splice(index, 1);
const nextColor = new AggregationColor(nextColors);
onChange(nextColor);
onChangeComplete(nextColor);
};
// ============================= Change =============================
const onInternalChangeComplete = (nextValues: number[]) => {
onChangeComplete(new AggregationColor(colorList));
// Reset `activeIndex` if out of range
if (activeIndex >= nextValues.length) {
onActive(nextValues.length - 1);
}
onGradientDragging(false);
};
// ============================= Render =============================
if (!isGradient) {
return null;
}
return (
<GradientColorSlider
min={0}
max={100}
prefixCls={prefixCls}
className={`${prefixCls}-gradient-slider`}
colors={colorList}
color={null!}
value={values}
range
onChangeComplete={onInternalChangeComplete}
disabled={false}
type="gradient"
// Active
activeIndex={activeIndex}
onActive={onActive}
// Drag
onDragStart={onDragStart}
onDragChange={onDragChange}
onKeyDelete={onKeyDelete}
/>
);
};
export default React.memo(GradientColorBar);

View File

@ -0,0 +1,188 @@
import type { FC } from 'react';
import React, { useContext } from 'react';
import RcColorPicker from '@rc-component/color-picker';
import type { Color } from '@rc-component/color-picker';
import useLayoutEffect from 'rc-util/lib/hooks/useLayoutEffect';
import Segmented from '../../../segmented';
import { AggregationColor } from '../../color';
import { PanelPickerContext } from '../../context';
import { genAlphaColor, generateColor } from '../../util';
import ColorClear from '../ColorClear';
import ColorInput from '../ColorInput';
import ColorSlider from '../ColorSlider';
import GradientColorBar from './GradientColorBar';
const components = {
slider: ColorSlider,
};
const PanelPicker: FC = () => {
const panelPickerContext = useContext(PanelPickerContext);
const {
mode,
onModeChange,
modeOptions,
prefixCls,
allowClear,
value,
disabledAlpha,
onChange,
onClear,
onChangeComplete,
activeIndex,
gradientDragging,
...injectProps
} = panelPickerContext;
// ============================ Colors ============================
const colors = React.useMemo(() => {
if (!value.cleared) {
return value.getColors();
}
return [
{
percent: 0,
color: new AggregationColor(''),
},
{
percent: 100,
color: new AggregationColor(''),
},
];
}, [value]);
// ========================= Single Color =========================
const isSingle = !value.isGradient();
// We cache the point color in case user drag the gradient point across another one
const [lockedColor, setLockedColor] = React.useState<AggregationColor>(value);
// Use layout effect here since `useEffect` will cause a blink when mouseDown
useLayoutEffect(() => {
if (!isSingle) {
setLockedColor(colors[activeIndex]?.color);
}
}, [gradientDragging, activeIndex]);
const activeColor = React.useMemo(() => {
if (isSingle) {
return value;
}
// Use cache when dragging. User can not operation panel when dragging.
if (gradientDragging) {
return lockedColor;
}
return colors[activeIndex]?.color;
}, [value, activeIndex, isSingle, lockedColor, gradientDragging]);
// ============================ Change ============================
const fillColor = (nextColor: AggregationColor) => {
if (mode === 'single') {
return nextColor;
}
const nextColors = [...colors];
nextColors[activeIndex] = {
...nextColors[activeIndex],
color: nextColor,
};
return new AggregationColor(nextColors);
};
const onInternalChange = (
colorValue: AggregationColor | Color,
fromPicker?: boolean,
info?: {
type?: 'hue' | 'alpha';
value?: number;
},
) => {
const nextColor = generateColor(colorValue);
let submitColor = nextColor;
if (value.cleared) {
const rgb = submitColor.toRgb();
// Auto fill color if origin is `0/0/0` to enhance user experience
if (!rgb.r && !rgb.g && !rgb.b && info) {
const { type: infoType, value: infoValue = 0 } = info;
submitColor = new AggregationColor({
h: infoType === 'hue' ? infoValue : 0,
s: 1,
b: 1,
a: infoType === 'alpha' ? infoValue / 100 : 1,
});
} else {
submitColor = genAlphaColor(submitColor);
}
}
onChange(fillColor(submitColor), fromPicker);
};
const onInternalChangeComplete = (nextColor: AggregationColor) => {
onChangeComplete(fillColor(nextColor));
};
// ============================ Render ============================
// Operation bar
let operationNode: React.ReactNode = null;
const showMode = modeOptions.length > 1;
if (allowClear || showMode) {
operationNode = (
<div className={`${prefixCls}-operation`}>
{showMode && (
<Segmented size="small" options={modeOptions} value={mode} onChange={onModeChange} />
)}
<ColorClear
prefixCls={prefixCls}
value={value}
onChange={(clearColor) => {
onChange(clearColor);
onClear?.();
}}
{...injectProps}
/>
</div>
);
}
// Return
return (
<>
{operationNode}
<GradientColorBar {...panelPickerContext} colors={colors} />
<RcColorPicker
prefixCls={prefixCls}
value={activeColor?.toHsb()}
disabledAlpha={disabledAlpha}
onChange={(colorValue, info) => {
onInternalChange(colorValue, true, info);
}}
onChangeComplete={(colorValue) => {
onInternalChangeComplete(generateColor(colorValue));
}}
components={components}
/>
<ColorInput
value={activeColor}
onChange={onInternalChange}
prefixCls={prefixCls}
disabledAlpha={disabledAlpha}
{...injectProps}
/>
</>
);
};
export default PanelPicker;

View File

@ -1,16 +1,9 @@
import type { FC } from 'react';
import React, { useContext } from 'react';
import type { Color } from '../color';
import { PanelPresetsContext } from '../context';
import type { ColorPickerBaseProps } from '../interface';
import ColorPresets from './ColorPresets';
export interface PanelPresetsProps extends Pick<ColorPickerBaseProps, 'prefixCls' | 'presets'> {
value?: Color;
onChange?: (value: Color) => void;
}
const PanelPresets: FC = () => {
const { prefixCls, value, presets, onChange } = useContext(PanelPresetsContext);
return Array.isArray(presets) ? (

View File

@ -1,11 +1,50 @@
import React from 'react';
import type { PanelPickerProps } from './components/PanelPicker';
import type { PanelPresetsProps } from './components/PanelPresets';
import type { GetProp } from '../_util/type';
import type { AggregationColor } from './color';
import type { ModeOptions } from './hooks/useModeColor';
import type { ColorFormatType, ColorPickerProps, ModeType, PresetsItem } from './interface';
export const PanelPickerContext = React.createContext<PanelPickerProps>({} as PanelPickerProps);
export interface PanelPickerContextProps {
prefixCls: string;
allowClear?: boolean;
disabled?: boolean;
disabledAlpha?: boolean;
mode: ModeType;
onModeChange: (mode: ModeType) => void;
modeOptions: ModeOptions;
export const PanelPresetsContext = React.createContext<PanelPresetsProps>({} as PanelPresetsProps);
value: AggregationColor;
onChange: (value?: AggregationColor, pickColor?: boolean) => void;
onChangeComplete: GetProp<ColorPickerProps, 'onChangeComplete'>;
export const { Provider: PanelPickerProvider } = PanelPickerContext;
export const { Provider: PanelPresetsProvider } = PanelPresetsContext;
format?: ColorFormatType;
onFormatChange?: ColorPickerProps['onFormatChange'];
/** The gradient Slider active handle */
activeIndex: number;
/** The gradient Slider handle active changed */
onActive: (index: number) => void;
/** Is gradient Slider dragging */
gradientDragging: boolean;
/** The gradient Slider dragging changed */
onGradientDragging: (dragging: boolean) => void;
onClear?: () => void;
}
export interface PanelPresetsContextProps {
prefixCls: string;
presets?: PresetsItem[];
disabled?: boolean;
value: AggregationColor;
onChange?: (value: AggregationColor) => void;
}
export const PanelPickerContext = React.createContext<PanelPickerContextProps>(
{} as PanelPickerContextProps,
);
export const PanelPresetsContext = React.createContext<PanelPresetsContextProps>(
{} as PanelPresetsContextProps,
);

View File

@ -2,7 +2,7 @@ import React, { useState } from 'react';
import { ColorPicker, Space } from 'antd';
import type { ColorPickerProps, GetProp } from 'antd';
type Color = GetProp<ColorPickerProps, 'value'>;
type Color = Extract<GetProp<ColorPickerProps, 'value'>, string | { cleared: any }>;
type Format = GetProp<ColorPickerProps, 'format'>;
const HexCase: React.FC = () => {
@ -28,7 +28,7 @@ const HexCase: React.FC = () => {
};
const HsbCase: React.FC = () => {
const [colorHsb, setColorHsb] = useState<ColorPickerProps['value']>('hsb(215, 91%, 100%)');
const [colorHsb, setColorHsb] = useState<Color>('hsb(215, 91%, 100%)');
const [formatHsb, setFormatHsb] = useState<ColorPickerProps['format']>('hsb');
const hsbString = React.useMemo(
@ -50,7 +50,7 @@ const HsbCase: React.FC = () => {
};
const RgbCase: React.FC = () => {
const [colorRgb, setColorRgb] = useState<ColorPickerProps['value']>('rgb(22, 119, 255)');
const [colorRgb, setColorRgb] = useState<Color>('rgb(22, 119, 255)');
const [formatRgb, setFormatRgb] = useState<ColorPickerProps['format']>('rgb');
const rgbString = React.useMemo(

View File

@ -0,0 +1,7 @@
## zh-CN
点击添加,拖拽或者删除。
## en-US
Click to add, drag out or keyboard delete.

View File

@ -0,0 +1,38 @@
import React from 'react';
import { ColorPicker, Space } from 'antd';
const DEFAULT_COLOR = [
{
color: 'rgb(16, 142, 233)',
percent: 0,
},
{
color: 'rgb(135, 208, 104)',
percent: 100,
},
];
const Demo = () => (
<Space direction="vertical">
<ColorPicker
defaultValue={DEFAULT_COLOR}
allowClear
showText
mode={['single', 'gradient']}
onChangeComplete={(color) => {
console.log(color.toCssString());
}}
/>
<ColorPicker
defaultValue={DEFAULT_COLOR}
allowClear
showText
mode="gradient"
onChangeComplete={(color) => {
console.log(color.toCssString());
}}
/>
</Space>
);
export default Demo;

View File

@ -6,7 +6,7 @@ const Demo = () => {
const [open, setOpen] = useState(false);
return (
<Space direction="vertical">
<ColorPicker defaultValue="#1677ff" showText />
<ColorPicker defaultValue="#1677ff" showText allowClear />
<ColorPicker
defaultValue="#1677ff"
showText={(color) => <span>Custom Text ({color.toHexString()})</span>}

View File

@ -2,7 +2,7 @@ import React, { useMemo, useState } from 'react';
import { Button, ColorPicker } from 'antd';
import type { ColorPickerProps, GetProp } from 'antd';
type Color = GetProp<ColorPickerProps, 'value'>;
type Color = Extract<GetProp<ColorPickerProps, 'value'>, string | { cleared: any }>;
const Demo: React.FC = () => {
const [color, setColor] = useState<Color>('#1677ff');

View File

@ -1,56 +0,0 @@
import { useEffect, useRef, useState } from 'react';
import type { Color } from '../color';
import type { ColorValueType } from '../interface';
import { generateColor } from '../util';
const INIT_COLOR_REF = {} as ColorValueType;
function hasValue(value?: ColorValueType) {
return value !== undefined;
}
const useColorState = (
defaultStateValue: ColorValueType,
option: { defaultValue?: ColorValueType; value?: ColorValueType },
) => {
const { defaultValue, value } = option;
const prevColor = useRef<Color>(generateColor(''));
const [colorValue, _setColorValue] = useState<Color>(() => {
let mergedState: ColorValueType | undefined;
if (hasValue(value)) {
mergedState = value;
} else if (hasValue(defaultValue)) {
mergedState = defaultValue;
} else {
mergedState = defaultStateValue;
}
const color = generateColor(mergedState || '');
prevColor.current = color;
return color;
});
const setColorValue = (color: Color) => {
_setColorValue(color);
prevColor.current = color;
};
const prevValue = useRef<ColorValueType | undefined>(INIT_COLOR_REF);
useEffect(() => {
// `useEffect` will be executed twice in strict mode even if the deps are the same
// So we compare the value manually to avoid unnecessary update
if (prevValue.current === value) {
return;
}
prevValue.current = value;
const newColor = generateColor(hasValue(value) ? value || '' : prevColor.current);
if (prevColor.current.cleared === true) {
newColor.cleared = 'controlled';
}
setColorValue(newColor);
}, [value]);
return [colorValue, setColorValue, prevColor] as const;
};
export default useColorState;

View File

@ -0,0 +1,96 @@
import * as React from 'react';
import { useEvent, useMergedState } from 'rc-util';
import { useLocale } from '../../locale';
import type { AggregationColor } from '../color';
import type { ColorPickerProps, ColorValueType, ModeType } from '../interface';
import { generateColor } from '../util';
export type ModeOptions = {
label: React.ReactNode;
value: ModeType;
}[];
/**
* Combine the `color` and `mode` to make sure sync of state.
*/
export default function useModeColor(
defaultValue?: ColorValueType,
value?: ColorValueType,
mode?: ColorPickerProps['mode'],
): [
color: AggregationColor,
setColor: (color: AggregationColor) => void,
mode: ModeType,
setMode: (mode: ModeType) => void,
modeOptionList: ModeOptions,
] {
const [locale] = useLocale('ColorPicker');
// ======================== Base ========================
// Color
const [mergedColor, setMergedColor] = useMergedState(defaultValue, { value });
// Mode
const [modeState, setModeState] = React.useState<ModeType>('single');
const [modeOptionList, modeSet] = React.useMemo(() => {
const list = (Array.isArray(mode) ? mode : [mode]).filter((m) => m);
if (!list.length) {
list.push('single');
}
const modes = new Set(list);
const optionList: ModeOptions = [];
const pushOption = (modeType: ModeType, localeTxt: string) => {
if (modes.has(modeType)) {
optionList.push({
label: localeTxt,
value: modeType,
});
}
};
pushOption('single', locale.singleColor);
pushOption('gradient', locale.gradientColor);
return [optionList, modes];
}, [mode]);
// ======================== Post ========================
// We need align `mode` with `color` state
// >>>>> Color
const [cacheColor, setCacheColor] = React.useState<AggregationColor | null>(null);
const setColor = useEvent((nextColor: AggregationColor) => {
setCacheColor(nextColor);
setMergedColor(nextColor);
});
const postColor = React.useMemo(() => {
const colorObj = generateColor(mergedColor || '');
// Use `cacheColor` in case the color is `cleared`
return colorObj.equals(cacheColor) ? cacheColor! : colorObj;
}, [mergedColor, cacheColor]);
// >>>>> Mode
const postMode = React.useMemo(() => {
if (modeSet.has(modeState)) {
return modeState;
}
return modeOptionList[0]?.value;
}, [modeSet, modeState, modeOptionList]);
// ======================= Effect =======================
// Dynamic update mode when color change
React.useEffect(() => {
setModeState(postColor.isGradient() ? 'gradient' : 'single');
}, [postColor]);
// ======================= Return =======================
return [postColor, setColor, postMode, setModeState, modeOptionList];
}

View File

@ -22,6 +22,7 @@ Used when the user needs to make a customized color selection.
<code src="./demo/size.tsx">Trigger size</code>
<code src="./demo/controlled.tsx">controlled mode</code>
<code src="./demo/change-completed.tsx">Color change completed</code>
<code src="./demo/line-gradient.tsx" version="5.20.0">Line Gradient</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>
@ -51,6 +52,7 @@ Common props ref[Common props](/docs/react/common-props)
| 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` | |
| mode | Configure single or gradient color | `('single' \| 'gradient')[]` | `single` | 5.20.0 |
| open | Whether to show popup | boolean | - | |
| presets | Preset colors | `{ label: ReactNode, colors: Array<string \| Color>, defaultOpen?: boolean }[]` | - | `defaultOpen: 5.11.0` |
| placement | Placement of popup | The design of the [placement](/components/tooltip/#api) parameter is the same as the `Tooltips` component. | `bottomLeft` | |
@ -68,8 +70,9 @@ Common props ref[Common props](/docs/react/common-props)
### Color
<!-- prettier-ignore -->
| Property | Description | Type | Default |
| Property | Description | Type | Version |
| :-- | :-- | :-- | :-- |
| toCssString | Convert to CSS support format | `() => string` | 5.20.0 |
| toHex | Convert to `hex` format characters, the return type like: `1677ff` | `() => string` | - |
| toHexString | Convert to `hex` format color string, the return type like: `#1677ff` | `() => string` | - |
| toHsb | Convert to `hsb` object | `() => ({ h: number, s: number, b: number, a number })` | - |

View File

@ -1,6 +1,6 @@
import ColorPicker from './ColorPicker';
export type { ColorPickerProps } from './interface';
export type { Color } from './color';
export type { AggregationColor as Color } from './color';
export default ColorPicker;

View File

@ -23,6 +23,7 @@ group:
<code src="./demo/size.tsx">触发器尺寸大小</code>
<code src="./demo/controlled.tsx">受控模式</code>
<code src="./demo/change-completed.tsx">颜色完成选择</code>
<code src="./demo/line-gradient.tsx" version="5.20.0">渐变色</code>
<code src="./demo/text-render.tsx">渲染触发器文本</code>
<code src="./demo/disabled.tsx">禁用</code>
<code src="./demo/disabled-alpha.tsx">禁用透明度</code>
@ -52,6 +53,7 @@ group:
| disabledAlpha | 禁用透明度 | boolean | - | 5.8.0 |
| destroyTooltipOnHide | 关闭后是否销毁弹窗 | `boolean` | false | 5.7.0 |
| format | 颜色格式 | `rgb` \| `hex` \| `hsb` | `hex` | |
| mode | 选择器模式,用于配置单色与渐变 | `('single' \| 'gradient')[]` | `single` | 5.20.0 |
| open | 是否显示弹出窗口 | boolean | - | |
| presets | 预设的颜色 | `{ label: ReactNode, colors: Array<string \| Color>, defaultOpen?: boolean }[]` | - | `defaultOpen: 5.11.0` |
| placement | 弹出窗口的位置 | 同 `Tooltips` 组件的 [placement](/components/tooltip-cn/#api) 参数设计 | `bottomLeft` | |
@ -69,8 +71,9 @@ group:
### Color
<!-- prettier-ignore -->
| 参数 | 说明 | 类型 | 默认值 |
| 参数 | 说明 | 类型 | 版本 |
| :-- | :-- | :-- | :-- |
| toCssString | 转换成 CSS 支持的格式 | `() => string` | 5.20.0 |
| toHex | 转换成 `hex` 格式字符,返回格式如:`1677ff` | `() => string` | - |
| toHexString | 转换成 `hex` 格式颜色字符串,返回格式如:`#1677ff` | `() => string` | - |
| toHsb | 转换成 `hsb` 对象 | `() => ({ h: number, s: number, b: number, a number })` | - |

View File

@ -1,10 +1,20 @@
import type { CSSProperties, FC, ReactNode } from 'react';
import type { ColorPickerProps as RcColorPickerProps } from '@rc-component/color-picker';
import type {
ColorGenInput,
ColorPickerProps as RcColorPickerProps,
} from '@rc-component/color-picker';
import type { SizeType } from '../config-provider/SizeContext';
import type { PopoverProps } from '../popover';
import type { TooltipPlacement } from '../tooltip';
import type { Color } from './color';
import type { AggregationColor } from './color';
export type { ColorGenInput };
export type Colors<T> = {
color: ColorGenInput<T>;
percent: number;
}[];
export enum ColorFormat {
hex = 'hex',
@ -16,7 +26,7 @@ export type ColorFormatType = keyof typeof ColorFormat;
export interface PresetsItem {
label: ReactNode;
colors: (string | Color)[];
colors: (string | AggregationColor)[];
/**
* Whether the initial state is collapsed
* @since 5.11.0
@ -28,25 +38,29 @@ export type TriggerType = 'click' | 'hover';
export type TriggerPlacement = TooltipPlacement; // Alias, to prevent breaking changes.
export interface ColorPickerBaseProps {
color?: Color;
prefixCls: string;
format?: ColorFormatType;
allowClear?: boolean;
disabled?: boolean;
disabledAlpha?: boolean;
presets?: PresetsItem[];
panelRender?: ColorPickerProps['panelRender'];
onFormatChange?: ColorPickerProps['onFormatChange'];
onChangeComplete?: ColorPickerProps['onChangeComplete'];
}
export type SingleValueType = AggregationColor | string;
export type ColorValueType = Color | string | null;
export type ColorValueType =
| SingleValueType
| null
| {
color: SingleValueType;
percent: number;
}[];
export type ModeType = 'single' | 'gradient';
export type ColorPickerProps = Omit<
RcColorPickerProps,
'onChange' | 'value' | 'defaultValue' | 'panelRender' | 'disabledAlpha' | 'onChangeComplete'
| 'onChange'
| 'value'
| 'defaultValue'
| 'panelRender'
| 'disabledAlpha'
| 'onChangeComplete'
| 'components'
> & {
mode?: ModeType | ModeType[];
value?: ColorValueType;
defaultValue?: ColorValueType;
children?: React.ReactNode;
@ -63,7 +77,7 @@ export type ColorPickerProps = Omit<
panel: React.ReactNode,
extra: { components: { Picker: FC; Presets: FC } },
) => React.ReactNode;
showText?: boolean | ((color: Color) => React.ReactNode);
showText?: boolean | ((color: AggregationColor) => React.ReactNode);
size?: SizeType;
styles?: { popup?: CSSProperties; popupOverlayInner?: CSSProperties };
rootClassName?: string;
@ -71,7 +85,7 @@ export type ColorPickerProps = Omit<
[key: `data-${string}`]: string;
onOpenChange?: (open: boolean) => void;
onFormatChange?: (format?: ColorFormatType) => void;
onChange?: (value: Color, hex: string) => void;
onChange?: (value: AggregationColor, hex: string) => void;
onClear?: () => void;
onChangeComplete?: (value: Color) => void;
onChangeComplete?: (value: AggregationColor) => void;
} & Pick<PopoverProps, 'getPopupContainer' | 'autoAdjustOverflow' | 'destroyTooltipOnHide'>;

View File

@ -21,11 +21,13 @@ const genColorBlockStyle = (token: ColorPickerToken, size: number): CSSObject =>
width: size,
height: size,
boxShadow: colorPickerInsetShadow,
flex: 'none',
...getTransBg('50%', token.colorFillSecondary),
[`${componentCls}-color-block-inner`]: {
width: '100%',
height: '100%',
border: `${unit(lineWidth)} solid ${colorFillSecondary}`,
boxShadow: `inset 0 0 0 ${unit(lineWidth)} ${colorFillSecondary}`,
borderRadius: 'inherit',
},
},

View File

@ -7,6 +7,7 @@ import genColorBlockStyle from './color-block';
import genInputStyle from './input';
import genPickerStyle from './picker';
import genPresetsStyle from './presets';
import genSliderStyle from './slider';
// biome-ignore lint/suspicious/noEmptyInterface: ComponentToken need to be empty by default
export interface ComponentToken {}
@ -74,12 +75,12 @@ const genClearStyle = (
'&::after': {
content: '""',
position: 'absolute',
insetInlineEnd: lineWidth,
top: 0,
insetInlineEnd: token.calc(lineWidth).mul(-1).equal(),
top: token.calc(lineWidth).mul(-1).equal(),
display: 'block',
width: 40, // maximum
height: 2, // fixed
transformOrigin: 'right',
transformOrigin: `calc(100% - 1px) 1px`,
transform: 'rotate(-45deg)',
backgroundColor: red6,
},
@ -138,7 +139,7 @@ const genSizeStyle = (token: ColorPickerToken): CSSObject => {
return {
[`&${componentCls}-lg`]: {
minWidth: controlHeightLG,
height: controlHeightLG,
minHeight: controlHeightLG,
borderRadius: borderRadiusLG,
[`${componentCls}-color-block, ${componentCls}-clear`]: {
width: controlHeight,
@ -151,13 +152,17 @@ const genSizeStyle = (token: ColorPickerToken): CSSObject => {
},
[`&${componentCls}-sm`]: {
minWidth: controlHeightSM,
height: controlHeightSM,
minHeight: controlHeightSM,
borderRadius: borderRadiusSM,
[`${componentCls}-color-block, ${componentCls}-clear`]: {
width: controlHeightXS,
height: controlHeightXS,
borderRadius: borderRadiusXS,
},
[`${componentCls}-trigger-text`]: {
lineHeight: controlHeightXS,
},
},
};
};
@ -206,23 +211,30 @@ const genColorPickerStyle: GenerateStyle<ColorPickerToken> = (token) => {
[`${componentCls}-panel`]: {
...genPickerStyle(token),
},
...genSliderStyle(token),
...genColorBlockStyle(token, colorPickerPreviewSize),
...genInputStyle(token),
...genPresetsStyle(token),
...genClearStyle(token, colorPickerPresetColorSize, {
marginInlineStart: 'auto',
marginBottom: marginXS,
}),
// Operation bar
[`${componentCls}-operation`]: {
display: 'flex',
justifyContent: 'space-between',
marginBottom: marginXS,
},
},
'&-trigger': {
minWidth: controlHeight,
height: controlHeight,
minHeight: controlHeight,
borderRadius,
border: `${unit(lineWidth)} solid ${colorBorder}`,
cursor: 'pointer',
display: 'inline-flex',
alignItems: 'center',
alignItems: 'flex-start',
justifyContent: 'center',
transition: `all ${motionDurationMid}`,
background: colorBgElevated,
@ -235,6 +247,17 @@ const genColorPickerStyle: GenerateStyle<ColorPickerToken> = (token) => {
.equal(),
fontSize,
color: colorText,
alignSelf: 'center',
'&-cell': {
'&:not(:last-child):after': {
content: '", "',
},
'&-inactive': {
color: colorTextDisabled,
},
},
},
'&:hover': {
borderColor: colorPrimaryHover,
@ -275,7 +298,7 @@ export default genStyleHooks('ColorPicker', (token) => {
colorPickerHandlerSizeSM: 12,
colorPickerAlphaInputWidth: 44,
colorPickerInputNumberHandleWidth: 16,
colorPickerPresetColorSize: 18,
colorPickerPresetColorSize: 24,
colorPickerInsetShadow: `inset 0 0 1px 0 ${colorTextQuaternary}`,
colorPickerSliderHeight,
colorPickerPreviewSize: token

View File

@ -2,7 +2,6 @@ import { unit } from '@ant-design/cssinjs';
import type { CSSObject } from '@ant-design/cssinjs';
import type { GenerateStyle } from '../../theme/internal';
import { getTransBg } from './color-block';
import type { ColorPickerToken } from './index';
const genPickerStyle: GenerateStyle<ColorPickerToken, CSSObject> = (token) => {
@ -16,11 +15,11 @@ const genPickerStyle: GenerateStyle<ColorPickerToken, CSSObject> = (token) => {
colorFillSecondary,
lineWidthBold,
colorPickerHandlerSize,
colorPickerHandlerSizeSM,
colorPickerSliderHeight,
} = token;
return {
userSelect: 'none',
[`${componentCls}-select`]: {
[`${componentCls}-palette`]: {
minHeight: token.calc(controlHeightLG).mul(4).equal(),
@ -36,6 +35,7 @@ const genPickerStyle: GenerateStyle<ColorPickerToken, CSSObject> = (token) => {
marginBottom: marginSM,
},
// ======================== Panel =========================
[`${componentCls}-handler`]: {
width: colorPickerHandlerSize,
height: colorPickerHandlerSize,
@ -44,40 +44,6 @@ const genPickerStyle: GenerateStyle<ColorPickerToken, CSSObject> = (token) => {
borderRadius: '50%',
cursor: 'pointer',
boxShadow: `${colorPickerInsetShadow}, 0 0 0 1px ${colorFillSecondary}`,
'&-sm': {
width: colorPickerHandlerSizeSM,
height: colorPickerHandlerSizeSM,
},
},
[`${componentCls}-slider`]: {
borderRadius: token.calc(colorPickerSliderHeight).div(2).equal(),
[`${componentCls}-palette`]: {
height: colorPickerSliderHeight,
},
[`${componentCls}-gradient`]: {
borderRadius: token.calc(colorPickerSliderHeight).div(2).equal(),
boxShadow: colorPickerInsetShadow,
},
'&-alpha': getTransBg(`${unit(colorPickerSliderHeight)}`, token.colorFillSecondary),
'&-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,
},
},
},
},
};
};

View File

@ -0,0 +1,126 @@
import { unit } from '@ant-design/cssinjs';
import type { CSSObject } from '@ant-design/cssinjs';
import type { GenerateStyle } from '../../theme/internal';
import { getTransBg } from './color-block';
import type { ColorPickerToken } from './index';
const genSliderStyle: GenerateStyle<ColorPickerToken, CSSObject> = (token) => {
const {
componentCls,
colorPickerInsetShadow,
colorBgElevated,
colorFillSecondary,
lineWidthBold,
colorPickerHandlerSizeSM,
colorPickerSliderHeight,
marginSM,
marginXS,
} = token;
const handleInnerSize = token
.calc(colorPickerHandlerSizeSM)
.sub(token.calc(lineWidthBold).mul(2).equal())
.equal();
const handleHoverSize = token
.calc(colorPickerHandlerSizeSM)
.add(token.calc(lineWidthBold).mul(2).equal())
.equal();
const activeHandleStyle = {
'&:after': {
transform: 'scale(1)',
boxShadow: `${colorPickerInsetShadow}, 0 0 0 1px ${token.colorPrimaryActive}`,
},
};
return {
// ======================== Slider ========================
[`${componentCls}-slider`]: [
getTransBg(`${unit(colorPickerSliderHeight)}`, token.colorFillSecondary),
{
margin: 0,
padding: 0,
height: colorPickerSliderHeight,
borderRadius: token.calc(colorPickerSliderHeight).div(2).equal(),
'&-rail': {
height: colorPickerSliderHeight,
borderRadius: token.calc(colorPickerSliderHeight).div(2).equal(),
boxShadow: colorPickerInsetShadow,
},
[`& ${componentCls}-slider-handle`]: {
width: handleInnerSize,
height: handleInnerSize,
top: 0,
borderRadius: '100%',
'&:before': {
display: 'block',
position: 'absolute',
background: 'transparent',
left: {
_skip_check_: true,
value: '50%',
},
top: '50%',
transform: 'translate(-50%, -50%)',
width: handleHoverSize,
height: handleHoverSize,
borderRadius: '100%',
},
'&:after': {
width: colorPickerHandlerSizeSM,
height: colorPickerHandlerSizeSM,
border: `${unit(lineWidthBold)} solid ${colorBgElevated}`,
boxShadow: `${colorPickerInsetShadow}, 0 0 0 1px ${colorFillSecondary}`,
outline: 'none',
insetInlineStart: token.calc(lineWidthBold).mul(-1).equal(),
top: token.calc(lineWidthBold).mul(-1).equal(),
background: 'transparent',
transition: 'none',
},
'&:focus': activeHandleStyle,
},
},
],
// ======================== Layout ========================
[`${componentCls}-slider-container`]: {
display: 'flex',
gap: marginSM,
marginBottom: marginSM,
// Group
[`${componentCls}-slider-group`]: {
flex: 1,
flexDirection: 'column',
justifyContent: 'space-between',
display: 'flex',
'&-disabled-alpha': {
justifyContent: 'center',
},
},
},
[`${componentCls}-gradient-slider`]: {
marginBottom: marginXS,
[`& ${componentCls}-slider-handle`]: {
'&:after': {
transform: 'scale(0.8)',
},
'&-active, &:focus': activeHandleStyle,
},
},
};
};
export default genSliderStyle;

View File

@ -1,21 +1,69 @@
import type { ColorGenInput } from '@rc-component/color-picker';
import { Color as RcColor } from '@rc-component/color-picker';
import type { Color } from './color';
import { ColorFactory } from './color';
import { AggregationColor } from './color';
import type { ColorValueType } from './interface';
export const generateColor = (color: ColorGenInput<Color>): Color => {
if (color instanceof ColorFactory) {
export const generateColor = (
color: ColorGenInput<AggregationColor> | Exclude<ColorValueType, null>,
): AggregationColor => {
if (color instanceof AggregationColor) {
return color;
}
return new ColorFactory(color);
return new AggregationColor(color);
};
export const getRoundNumber = (value: number) => Math.round(Number(value || 0));
export const getAlphaColor = (color: Color) => getRoundNumber(color.toHsb().a * 100);
export const getColorAlpha = (color: AggregationColor) => getRoundNumber(color.toHsb().a * 100);
export const genAlphaColor = (color: Color, alpha?: number) => {
/** Return the color whose `alpha` is 1 */
export const genAlphaColor = (color: AggregationColor, alpha?: number) => {
const hsba = color.toHsb();
hsba.a = alpha || 1;
return generateColor(hsba);
};
/**
* Get percent position color. e.g. [10%-#fff, 20%-#000], 15% => #888
*/
export const getGradientPercentColor = (
colors: { percent: number; color: string }[],
percent: number,
): string => {
const filledColors = [
{
percent: 0,
color: colors[0].color,
},
...colors,
{
percent: 100,
color: colors[colors.length - 1].color,
},
];
for (let i = 0; i < filledColors.length - 1; i += 1) {
const startPtg = filledColors[i].percent;
const endPtg = filledColors[i + 1].percent;
const startColor = filledColors[i].color;
const endColor = filledColors[i + 1].color;
if (startPtg <= percent && percent <= endPtg) {
const dist = endPtg - startPtg;
if (dist === 0) {
return startColor;
}
const ratio = ((percent - startPtg) / dist) * 100;
const startRcColor = new RcColor(startColor);
const endRcColor = new RcColor(endColor);
return startRcColor.mix(endRcColor, ratio).toRgbString();
}
}
// This will never reach
/* istanbul ignore next */
return '';
};

View File

@ -0,0 +1,438 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`renderEmpty should render Cascader empty 1`] = `
<div
class="ant-empty ant-empty-normal ant-empty-small"
>
<div
class="ant-empty-image"
>
<svg
height="41"
viewBox="0 0 64 41"
width="64"
xmlns="http://www.w3.org/2000/svg"
>
<title>
Simple Empty
</title>
<g
fill="none"
fill-rule="evenodd"
transform="translate(0 1)"
>
<ellipse
cx="32"
cy="33"
fill="#f5f5f5"
rx="32"
ry="7"
/>
<g
fill-rule="nonzero"
stroke="#d9d9d9"
>
<path
d="M55 12.76L44.854 1.258C44.367.474 43.656 0 42.907 0H21.093c-.749 0-1.46.474-1.947 1.257L9 12.761V22h46v-9.24z"
/>
<path
d="M41.613 15.931c0-1.605.994-2.93 2.227-2.931H55v18.137C55 33.26 53.68 35 52.05 35h-40.1C10.32 35 9 33.259 9 31.137V13h11.16c1.233 0 2.227 1.323 2.227 2.928v.022c0 1.605 1.005 2.901 2.237 2.901h14.752c1.232 0 2.237-1.308 2.237-2.913v-.007z"
fill="#fafafa"
/>
</g>
</g>
</svg>
</div>
<div
class="ant-empty-description"
>
No data
</div>
</div>
`;
exports[`renderEmpty should render List empty 1`] = `
<div
class="ant-empty ant-empty-normal"
>
<div
class="ant-empty-image"
>
<svg
height="41"
viewBox="0 0 64 41"
width="64"
xmlns="http://www.w3.org/2000/svg"
>
<title>
Simple Empty
</title>
<g
fill="none"
fill-rule="evenodd"
transform="translate(0 1)"
>
<ellipse
cx="32"
cy="33"
fill="#f5f5f5"
rx="32"
ry="7"
/>
<g
fill-rule="nonzero"
stroke="#d9d9d9"
>
<path
d="M55 12.76L44.854 1.258C44.367.474 43.656 0 42.907 0H21.093c-.749 0-1.46.474-1.947 1.257L9 12.761V22h46v-9.24z"
/>
<path
d="M41.613 15.931c0-1.605.994-2.93 2.227-2.931H55v18.137C55 33.26 53.68 35 52.05 35h-40.1C10.32 35 9 33.259 9 31.137V13h11.16c1.233 0 2.227 1.323 2.227 2.928v.022c0 1.605 1.005 2.901 2.237 2.901h14.752c1.232 0 2.237-1.308 2.237-2.913v-.007z"
fill="#fafafa"
/>
</g>
</g>
</svg>
</div>
<div
class="ant-empty-description"
>
No data
</div>
</div>
`;
exports[`renderEmpty should render Mentions empty 1`] = `
<div
class="ant-empty ant-empty-normal ant-empty-small"
>
<div
class="ant-empty-image"
>
<svg
height="41"
viewBox="0 0 64 41"
width="64"
xmlns="http://www.w3.org/2000/svg"
>
<title>
Simple Empty
</title>
<g
fill="none"
fill-rule="evenodd"
transform="translate(0 1)"
>
<ellipse
cx="32"
cy="33"
fill="#f5f5f5"
rx="32"
ry="7"
/>
<g
fill-rule="nonzero"
stroke="#d9d9d9"
>
<path
d="M55 12.76L44.854 1.258C44.367.474 43.656 0 42.907 0H21.093c-.749 0-1.46.474-1.947 1.257L9 12.761V22h46v-9.24z"
/>
<path
d="M41.613 15.931c0-1.605.994-2.93 2.227-2.931H55v18.137C55 33.26 53.68 35 52.05 35h-40.1C10.32 35 9 33.259 9 31.137V13h11.16c1.233 0 2.227 1.323 2.227 2.928v.022c0 1.605 1.005 2.901 2.237 2.901h14.752c1.232 0 2.237-1.308 2.237-2.913v-.007z"
fill="#fafafa"
/>
</g>
</g>
</svg>
</div>
<div
class="ant-empty-description"
>
No data
</div>
</div>
`;
exports[`renderEmpty should render Select empty 1`] = `
<div
class="ant-empty ant-empty-normal ant-empty-small"
>
<div
class="ant-empty-image"
>
<svg
height="41"
viewBox="0 0 64 41"
width="64"
xmlns="http://www.w3.org/2000/svg"
>
<title>
Simple Empty
</title>
<g
fill="none"
fill-rule="evenodd"
transform="translate(0 1)"
>
<ellipse
cx="32"
cy="33"
fill="#f5f5f5"
rx="32"
ry="7"
/>
<g
fill-rule="nonzero"
stroke="#d9d9d9"
>
<path
d="M55 12.76L44.854 1.258C44.367.474 43.656 0 42.907 0H21.093c-.749 0-1.46.474-1.947 1.257L9 12.761V22h46v-9.24z"
/>
<path
d="M41.613 15.931c0-1.605.994-2.93 2.227-2.931H55v18.137C55 33.26 53.68 35 52.05 35h-40.1C10.32 35 9 33.259 9 31.137V13h11.16c1.233 0 2.227 1.323 2.227 2.928v.022c0 1.605 1.005 2.901 2.237 2.901h14.752c1.232 0 2.237-1.308 2.237-2.913v-.007z"
fill="#fafafa"
/>
</g>
</g>
</svg>
</div>
<div
class="ant-empty-description"
>
No data
</div>
</div>
`;
exports[`renderEmpty should render Table empty 1`] = `
<div
class="ant-empty ant-empty-normal"
>
<div
class="ant-empty-image"
>
<svg
height="41"
viewBox="0 0 64 41"
width="64"
xmlns="http://www.w3.org/2000/svg"
>
<title>
Simple Empty
</title>
<g
fill="none"
fill-rule="evenodd"
transform="translate(0 1)"
>
<ellipse
cx="32"
cy="33"
fill="#f5f5f5"
rx="32"
ry="7"
/>
<g
fill-rule="nonzero"
stroke="#d9d9d9"
>
<path
d="M55 12.76L44.854 1.258C44.367.474 43.656 0 42.907 0H21.093c-.749 0-1.46.474-1.947 1.257L9 12.761V22h46v-9.24z"
/>
<path
d="M41.613 15.931c0-1.605.994-2.93 2.227-2.931H55v18.137C55 33.26 53.68 35 52.05 35h-40.1C10.32 35 9 33.259 9 31.137V13h11.16c1.233 0 2.227 1.323 2.227 2.928v.022c0 1.605 1.005 2.901 2.237 2.901h14.752c1.232 0 2.237-1.308 2.237-2.913v-.007z"
fill="#fafafa"
/>
</g>
</g>
</svg>
</div>
<div
class="ant-empty-description"
>
No data
</div>
</div>
`;
exports[`renderEmpty should render Table.filter empty 1`] = `null`;
exports[`renderEmpty should render Transfer empty 1`] = `
<div
class="ant-empty ant-empty-normal ant-empty-small"
>
<div
class="ant-empty-image"
>
<svg
height="41"
viewBox="0 0 64 41"
width="64"
xmlns="http://www.w3.org/2000/svg"
>
<title>
Simple Empty
</title>
<g
fill="none"
fill-rule="evenodd"
transform="translate(0 1)"
>
<ellipse
cx="32"
cy="33"
fill="#f5f5f5"
rx="32"
ry="7"
/>
<g
fill-rule="nonzero"
stroke="#d9d9d9"
>
<path
d="M55 12.76L44.854 1.258C44.367.474 43.656 0 42.907 0H21.093c-.749 0-1.46.474-1.947 1.257L9 12.761V22h46v-9.24z"
/>
<path
d="M41.613 15.931c0-1.605.994-2.93 2.227-2.931H55v18.137C55 33.26 53.68 35 52.05 35h-40.1C10.32 35 9 33.259 9 31.137V13h11.16c1.233 0 2.227 1.323 2.227 2.928v.022c0 1.605 1.005 2.901 2.237 2.901h14.752c1.232 0 2.237-1.308 2.237-2.913v-.007z"
fill="#fafafa"
/>
</g>
</g>
</svg>
</div>
<div
class="ant-empty-description"
>
No data
</div>
</div>
`;
exports[`renderEmpty should render TreeSelect empty 1`] = `
<div
class="ant-empty ant-empty-normal ant-empty-small"
>
<div
class="ant-empty-image"
>
<svg
height="41"
viewBox="0 0 64 41"
width="64"
xmlns="http://www.w3.org/2000/svg"
>
<title>
Simple Empty
</title>
<g
fill="none"
fill-rule="evenodd"
transform="translate(0 1)"
>
<ellipse
cx="32"
cy="33"
fill="#f5f5f5"
rx="32"
ry="7"
/>
<g
fill-rule="nonzero"
stroke="#d9d9d9"
>
<path
d="M55 12.76L44.854 1.258C44.367.474 43.656 0 42.907 0H21.093c-.749 0-1.46.474-1.947 1.257L9 12.761V22h46v-9.24z"
/>
<path
d="M41.613 15.931c0-1.605.994-2.93 2.227-2.931H55v18.137C55 33.26 53.68 35 52.05 35h-40.1C10.32 35 9 33.259 9 31.137V13h11.16c1.233 0 2.227 1.323 2.227 2.928v.022c0 1.605 1.005 2.901 2.237 2.901h14.752c1.232 0 2.237-1.308 2.237-2.913v-.007z"
fill="#fafafa"
/>
</g>
</g>
</svg>
</div>
<div
class="ant-empty-description"
>
No data
</div>
</div>
`;
exports[`renderEmpty should return empty when componentName is not matched 1`] = `
<div
class="ant-empty"
>
<div
class="ant-empty-image"
>
<svg
height="152"
viewBox="0 0 184 152"
width="184"
xmlns="http://www.w3.org/2000/svg"
>
<title>
empty image
</title>
<g
fill="none"
fill-rule="evenodd"
>
<g
transform="translate(24 31.67)"
>
<ellipse
cx="67.797"
cy="106.89"
fill="#F5F5F7"
fill-opacity=".8"
rx="67.797"
ry="12.668"
/>
<path
d="M122.034 69.674L98.109 40.229c-1.148-1.386-2.826-2.225-4.593-2.225h-51.44c-1.766 0-3.444.839-4.592 2.225L13.56 69.674v15.383h108.475V69.674z"
fill="#AEB8C2"
/>
<path
d="M101.537 86.214L80.63 61.102c-1.001-1.207-2.507-1.867-4.048-1.867H31.724c-1.54 0-3.047.66-4.048 1.867L6.769 86.214v13.792h94.768V86.214z"
fill="url(#linearGradient-1)"
transform="translate(13.56)"
/>
<path
d="M33.83 0h67.933a4 4 0 0 1 4 4v93.344a4 4 0 0 1-4 4H33.83a4 4 0 0 1-4-4V4a4 4 0 0 1 4-4z"
fill="#F5F5F7"
/>
<path
d="M42.678 9.953h50.237a2 2 0 0 1 2 2V36.91a2 2 0 0 1-2 2H42.678a2 2 0 0 1-2-2V11.953a2 2 0 0 1 2-2zM42.94 49.767h49.713a2.262 2.262 0 1 1 0 4.524H42.94a2.262 2.262 0 0 1 0-4.524zM42.94 61.53h49.713a2.262 2.262 0 1 1 0 4.525H42.94a2.262 2.262 0 0 1 0-4.525zM121.813 105.032c-.775 3.071-3.497 5.36-6.735 5.36H20.515c-3.238 0-5.96-2.29-6.734-5.36a7.309 7.309 0 0 1-.222-1.79V69.675h26.318c2.907 0 5.25 2.448 5.25 5.42v.04c0 2.971 2.37 5.37 5.277 5.37h34.785c2.907 0 5.277-2.421 5.277-5.393V75.1c0-2.972 2.343-5.426 5.25-5.426h26.318v33.569c0 .617-.077 1.216-.221 1.789z"
fill="#DCE0E6"
/>
</g>
<path
d="M149.121 33.292l-6.83 2.65a1 1 0 0 1-1.317-1.23l1.937-6.207c-2.589-2.944-4.109-6.534-4.109-10.408C138.802 8.102 148.92 0 161.402 0 173.881 0 184 8.102 184 18.097c0 9.995-10.118 18.097-22.599 18.097-4.528 0-8.744-1.066-12.28-2.902z"
fill="#DCE0E6"
/>
<g
fill="#FFF"
transform="translate(149.65 15.383)"
>
<ellipse
cx="20.654"
cy="3.167"
rx="2.849"
ry="2.815"
/>
<path
d="M5.698 5.63H0L2.898.704zM9.259.704h4.985V5.63H9.259z"
/>
</g>
</g>
</svg>
</div>
<div
class="ant-empty-description"
>
No data
</div>
</div>
`;

View File

@ -0,0 +1,30 @@
import React from 'react';
import RenderEmpty from '../defaultRenderEmpty';
import { render } from '../../../tests/utils';
describe('renderEmpty', () => {
it.each([
'Table',
'Table.filter' /* 👈 5.19.0+ */,
'List',
'Select',
'TreeSelect',
'Cascader',
'Transfer',
'Mentions',
])('should render %s empty', (componentName: any) => {
const { container } = render(<RenderEmpty componentName={componentName} />);
expect(container.firstChild).toMatchSnapshot();
});
// https://github.com/ant-design/ant-design/pull/49613#issuecomment-2198857047
it('should return false when componentName is `Table.filter`', () => {
const { container } = render(<RenderEmpty componentName="Table.filter" />);
expect(container.firstChild).toBeFalsy();
});
it('should return empty when componentName is not matched', () => {
const { container } = render(<RenderEmpty componentName={`not_match` as any} />);
expect(container.firstChild).toMatchSnapshot();
});
});

View File

@ -24,6 +24,7 @@ import type { ArgsProps } from '../notification/interface';
import type { PaginationProps } from '../pagination';
import type { SelectProps } from '../select';
import type { SpaceProps } from '../space';
import type { SpinProps } from '../spin';
import type { TableProps } from '../table';
import type { TabsProps } from '../tabs';
import type { TagProps } from '../tag';
@ -175,6 +176,8 @@ export type SelectConfig = ComponentStyleConfig & Pick<SelectProps, 'showSearch'
export type SpaceConfig = ComponentStyleConfig & Pick<SpaceProps, 'size' | 'classNames' | 'styles'>;
export type SpinConfig = ComponentStyleConfig & Pick<SpinProps, 'indicator'>;
export type InputNumberConfig = ComponentStyleConfig & Pick<InputNumberProps, 'variant'>;
export type CascaderConfig = ComponentStyleConfig & Pick<CascaderProps, 'variant'>;
@ -254,7 +257,7 @@ export interface ConfigConsumerProps {
floatButtonGroup?: FloatButtonGroupConfig;
typography?: ComponentStyleConfig;
skeleton?: ComponentStyleConfig;
spin?: ComponentStyleConfig;
spin?: SpinConfig;
segmented?: ComponentStyleConfig;
steps?: ComponentStyleConfig;
statistic?: ComponentStyleConfig;

View File

@ -6,6 +6,7 @@ import Empty from '../empty';
type ComponentName =
| 'Table'
| 'Table.filter' /* 👈 5.20.0+ */
| 'List'
| 'Select'
| 'TreeSelect'
@ -31,7 +32,14 @@ const DefaultRenderEmpty: React.FC<EmptyProps> = (props) => {
case 'Transfer':
case 'Mentions':
return <Empty image={Empty.PRESENTED_IMAGE_SIMPLE} className={`${prefix}-small`} />;
/* istanbul ignore next */
/**
* This type of component should satisfy the nullish coalescing operator(??) on the left-hand side.
* to let the component itself implement the logic.
* For example `Table.filter`.
*/
case 'Table.filter':
// why `null`? legacy react16 node type `undefined` is not allowed.
return null;
default:
// Should never hit if we take all the component into consider.
return <Empty />;

View File

@ -12,7 +12,7 @@ import {
} from 'antd';
import type { ColorPickerProps, GetProp } from 'antd';
type Color = Exclude<GetProp<ColorPickerProps, 'value'>, string>;
type Color = Extract<GetProp<ColorPickerProps, 'value'>, { cleared: any }>;
type ThemeData = {
borderRadius: number;

View File

@ -152,7 +152,7 @@ const {
| slider | Set Slider common props | { className?: string, style?: React.CSSProperties } | - | 5.7.0 |
| switch | Set Switch common props | { className?: string, style?: React.CSSProperties } | - | 5.7.0 |
| space | Set Space common props, ref [Space](/components/space) | { size: `small` \| `middle` \| `large` \| `number`, className?: string, style?: React.CSSProperties, classNames?: { item: string }, styles?: { item: React.CSSProperties } } | - | 5.6.0 |
| spin | Set Spin common props | { className?: string, style?: React.CSSProperties } | - | 5.7.0 |
| spin | Set Spin common props | { className?: string, style?: React.CSSProperties, indicator?: React.ReactElement } | - | 5.7.0, indicator: 5.20.0 |
| statistic | Set Statistic common props | { className?: string, style?: React.CSSProperties } | - | 5.7.0 |
| steps | Set Steps common props | { className?: string, style?: React.CSSProperties } | - | 5.7.0 |
| table | Set Table common props | { className?: string, style?: React.CSSProperties, expandable?: { expandIcon?: props => React.ReactNode } } | - | 5.7.0, expandable: 5.14.0 |

View File

@ -43,6 +43,7 @@ import type {
RangePickerConfig,
SelectConfig,
SpaceConfig,
SpinConfig,
TableConfig,
TabsConfig,
TagConfig,
@ -189,7 +190,7 @@ export interface ConfigProviderProps {
drawer?: DrawerConfig;
typography?: ComponentStyleConfig;
skeleton?: ComponentStyleConfig;
spin?: ComponentStyleConfig;
spin?: SpinConfig;
segmented?: ComponentStyleConfig;
statistic?: ComponentStyleConfig;
steps?: ComponentStyleConfig;

View File

@ -154,7 +154,7 @@ const {
| slider | 设置 Slider 组件的通用属性 | { className?: string, style?: React.CSSProperties } | - | 5.7.0 |
| switch | 设置 Switch 组件的通用属性 | { className?: string, style?: React.CSSProperties } | - | 5.7.0 |
| space | 设置 Space 的通用属性,参考 [Space](/components/space-cn) | { size: `small` \| `middle` \| `large` \| `number`, className?: string, style?: React.CSSProperties, classNames?: { item: string }, styles?: { item: React.CSSProperties } } | - | 5.6.0 |
| spin | 设置 Spin 组件的通用属性 | { className?: string, style?: React.CSSProperties } | - | 5.7.0 |
| spin | 设置 Spin 组件的通用属性 | { className?: string, style?: React.CSSProperties, indicator?: React.ReactElement } | - | 5.7.0, indicator: 5.20.0 |
| statistic | 设置 Statistic 组件的通用属性 | { className?: string, style?: React.CSSProperties } | - | 5.7.0 |
| steps | 设置 Steps 组件的通用属性 | { className?: string, style?: React.CSSProperties } | - | 5.7.0 |
| table | 设置 Table 组件的通用属性 | { className?: string, style?: React.CSSProperties, expandable?: { expandIcon?: props => React.ReactNode } } | - | 5.7.0, expandable: 5.14.0 |

View File

@ -203,6 +203,58 @@ Array [
exports[`renders components/divider/demo/plain.tsx extend context correctly 2`] = `[]`;
exports[`renders components/divider/demo/variant.tsx extend context correctly 1`] = `
Array [
<p>
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed nonne merninisti licere mihi ista probare, quae sunt a te dicta? Refert tamen, quo modo.
</p>,
<div
class="ant-divider ant-divider-horizontal ant-divider-with-text ant-divider-with-text-center"
role="separator"
style="border-color: #7cb305;"
>
<span
class="ant-divider-inner-text"
>
Solid
</span>
</div>,
<p>
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed nonne merninisti licere mihi ista probare, quae sunt a te dicta? Refert tamen, quo modo.
</p>,
<div
class="ant-divider ant-divider-horizontal ant-divider-with-text ant-divider-with-text-center ant-divider-dotted"
role="separator"
style="border-color: #7cb305;"
>
<span
class="ant-divider-inner-text"
>
Dotted
</span>
</div>,
<p>
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed nonne merninisti licere mihi ista probare, quae sunt a te dicta? Refert tamen, quo modo.
</p>,
<div
class="ant-divider ant-divider-horizontal ant-divider-with-text ant-divider-with-text-center ant-divider-dashed"
role="separator"
style="border-color: #7cb305;"
>
<span
class="ant-divider-inner-text"
>
Dashed
</span>
</div>,
<p>
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed nonne merninisti licere mihi ista probare, quae sunt a te dicta? Refert tamen, quo modo.
</p>,
]
`;
exports[`renders components/divider/demo/variant.tsx extend context correctly 2`] = `[]`;
exports[`renders components/divider/demo/vertical.tsx extend context correctly 1`] = `
Array [
Text,

View File

@ -195,6 +195,56 @@ Array [
]
`;
exports[`renders components/divider/demo/variant.tsx correctly 1`] = `
Array [
<p>
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed nonne merninisti licere mihi ista probare, quae sunt a te dicta? Refert tamen, quo modo.
</p>,
<div
class="ant-divider ant-divider-horizontal ant-divider-with-text ant-divider-with-text-center"
role="separator"
style="border-color:#7cb305"
>
<span
class="ant-divider-inner-text"
>
Solid
</span>
</div>,
<p>
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed nonne merninisti licere mihi ista probare, quae sunt a te dicta? Refert tamen, quo modo.
</p>,
<div
class="ant-divider ant-divider-horizontal ant-divider-with-text ant-divider-with-text-center ant-divider-dotted"
role="separator"
style="border-color:#7cb305"
>
<span
class="ant-divider-inner-text"
>
Dotted
</span>
</div>,
<p>
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed nonne merninisti licere mihi ista probare, quae sunt a te dicta? Refert tamen, quo modo.
</p>,
<div
class="ant-divider ant-divider-horizontal ant-divider-with-text ant-divider-with-text-center ant-divider-dashed"
role="separator"
style="border-color:#7cb305"
>
<span
class="ant-divider-inner-text"
>
Dashed
</span>
</div>,
<p>
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed nonne merninisti licere mihi ista probare, quae sunt a te dicta? Refert tamen, quo modo.
</p>,
]
`;
exports[`renders components/divider/demo/vertical.tsx correctly 1`] = `
Array [
Text,

View File

@ -37,4 +37,15 @@ describe('Divider', () => {
borderStyle: 'dashed',
});
});
it('support string variant', () => {
const { container } = render(
<Divider variant="dotted">
test dotted
</Divider>,
);
expect(container?.querySelector<HTMLSpanElement>('.ant-divider-dotted')).toHaveStyle({
borderStyle: 'dotted',
});
});
});

View File

@ -0,0 +1,7 @@
## zh-CN
分隔线默认为 `solid`(实线)变体。您可以将其更改为 `dashed`(虚线)或 `dotted`(点线)。
## en-US
Divider is of `solid` variant by default. You can change that to either `dashed` or `dotted`.

View File

@ -0,0 +1,28 @@
import React from 'react';
import { Divider } from 'antd';
const App: React.FC = () => (
<>
<p>
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed nonne merninisti licere mihi ista
probare, quae sunt a te dicta? Refert tamen, quo modo.
</p>
<Divider style={{ borderColor: '#7cb305' }}>Solid</Divider>
<p>
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed nonne merninisti licere mihi ista
probare, quae sunt a te dicta? Refert tamen, quo modo.
</p>
<Divider variant="dotted" style={{ borderColor: '#7cb305' }}>Dotted</Divider>
<p>
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed nonne merninisti licere mihi ista
probare, quae sunt a te dicta? Refert tamen, quo modo.
</p>
<Divider variant="dashed" style={{ borderColor: '#7cb305' }} dashed >Dashed</Divider>
<p>
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed nonne merninisti licere mihi ista
probare, quae sunt a te dicta? Refert tamen, quo modo.
</p>
</>
);
export default App;

View File

@ -25,6 +25,7 @@ group:
<code src="./demo/vertical.tsx">Vertical</code>
<code src="./demo/customize-style.tsx" debug>Style Customization</code>
<code src="./demo/component-token.tsx" debug>Component Token</code>
<code src="./demo/variant.tsx">Variant</code>
## API
@ -35,6 +36,7 @@ Common props ref[Common props](/docs/react/common-props)
| children | The wrapped title | ReactNode | - | |
| className | The className of container | string | - | |
| dashed | Whether line is dashed | boolean | false | |
| variant | Whether line is dashed, dotted or solid | `dashed` \| `dotted` \| `solid` | solid | |
| orientation | The position of title inside divider | `left` \| `right` \| `center` | `center` | |
| orientationMargin | The margin-left/right between the title and its closest border, while the `orientation` must be `left` or `right`, If a numeric value of type `string` is provided without a unit, it is assumed to be in pixels (px) by default. | string \| number | - | |
| plain | Divider text show as plain style | boolean | true | 4.2.0 |

View File

@ -14,6 +14,7 @@ export interface DividerProps {
rootClassName?: string;
children?: React.ReactNode;
dashed?: boolean;
variant?: 'dashed' | 'dotted' | 'solid'
style?: React.CSSProperties;
plain?: boolean;
}
@ -30,6 +31,7 @@ const Divider: React.FC<DividerProps> = (props) => {
rootClassName,
children,
dashed,
variant = 'solid',
plain,
style,
...restProps
@ -51,6 +53,7 @@ const Divider: React.FC<DividerProps> = (props) => {
[`${prefixCls}-with-text`]: hasChildren,
[`${prefixCls}-with-text-${orientation}`]: hasChildren,
[`${prefixCls}-dashed`]: !!dashed,
[`${prefixCls}-${variant}`]: variant !== 'solid',
[`${prefixCls}-plain`]: !!plain,
[`${prefixCls}-rtl`]: direction === 'rtl',
[`${prefixCls}-no-default-orientation-margin-left`]: hasCustomMarginLeft,

View File

@ -26,6 +26,7 @@ group:
<code src="./demo/vertical.tsx">垂直分割线</code>
<code src="./demo/customize-style.tsx" debug>样式自定义</code>
<code src="./demo/component-token.tsx" debug>组件 Token</code>
<code src="./demo/variant.tsx">变体</code>
## API
@ -36,6 +37,7 @@ group:
| children | 嵌套的标题 | ReactNode | - | |
| className | 分割线样式类 | string | - | |
| dashed | 是否虚线 | boolean | false | |
| variant | 分割线是虚线、点线还是实线 | `dashed` \| `dotted` \| `solid` | solid | |
| orientation | 分割线标题的位置 | `left` \| `right` \| `center` | `center` | |
| orientationMargin | 标题和最近 left/right 边框之间的距离,去除了分割线,同时 `orientation` 必须为 `left``right`。如果传入 `string` 类型的数字且不带单位,默认单位是 px | string \| number | - | |
| plain | 文字是否显示为普通正文样式 | boolean | false | 4.2.0 |

View File

@ -136,6 +136,26 @@ const genSharedDividerStyle: GenerateStyle<DividerToken> = (token): CSSObject =>
borderBlockEnd: 0,
},
'&-dotted': {
background: 'none',
borderColor: colorSplit,
borderStyle: 'dotted',
borderWidth: `${unit(lineWidth)} 0 0`,
},
[`&-horizontal${componentCls}-with-text${componentCls}-dotted`]: {
'&::before, &::after': {
borderStyle: 'dotted none none',
},
},
[`&-vertical${componentCls}-dotted`]: {
borderInlineStartWidth: lineWidth,
borderInlineEnd: 0,
borderBlockStart: 0,
borderBlockEnd: 0,
},
[`&-plain${componentCls}-with-text`]: {
color: token.colorText,
fontWeight: 'normal',

View File

@ -11,7 +11,7 @@ interface EmptyToken extends FullToken<'Empty'> {
emptyImgCls: string;
emptyImgHeight: number | string;
emptyImgHeightSM: number | string;
emptyImgHeightMD: number;
emptyImgHeightMD: number | string;
}
// ============================== Shared ==============================

View File

@ -5309,7 +5309,7 @@ Array [
style="position: relative;"
>
<div
style="position: absolute; left: 0px; top: 0px; z-index: 1;"
style="position: absolute; left: 0%; top: 100%; z-index: 1; transform: translate(-50%, -50%);"
>
<div
class="ant-color-picker-handler"
@ -5329,46 +5329,46 @@ Array [
class="ant-color-picker-slider-group"
>
<div
class="ant-color-picker-slider ant-color-picker-slider-hue"
class="ant-slider ant-color-picker-slider ant-slider-horizontal"
>
<div
class="ant-color-picker-palette"
style="position: relative;"
>
class="ant-slider-rail ant-color-picker-slider-rail"
/>
<div
style="position: absolute; left: 0px; top: 0px; z-index: 1;"
>
class="ant-slider-step"
/>
<div
class="ant-color-picker-handler ant-color-picker-handler-sm"
style="background-color: rgb(255, 0, 0);"
aria-disabled="false"
aria-orientation="horizontal"
aria-valuemax="359"
aria-valuemin="0"
aria-valuenow="0"
class="ant-slider-handle ant-slider-handle-1 ant-color-picker-slider-handle"
role="slider"
style="left: 0%; transform: translateX(-50%); background: rgb(255, 0, 0);"
tabindex="0"
/>
</div>
<div
class="ant-color-picker-gradient"
style="position: absolute; inset: 0;"
/>
</div>
</div>
<div
class="ant-color-picker-slider ant-color-picker-slider-alpha"
class="ant-slider ant-color-picker-slider ant-slider-horizontal"
>
<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: rgba(0, 0, 0, 0);"
class="ant-slider-rail ant-color-picker-slider-rail"
/>
</div>
<div
class="ant-color-picker-gradient"
style="position: absolute; inset: 0;"
class="ant-slider-step"
/>
<div
aria-disabled="false"
aria-orientation="horizontal"
aria-valuemax="100"
aria-valuemin="0"
aria-valuenow="0"
class="ant-slider-handle ant-slider-handle-1 ant-color-picker-slider-handle"
role="slider"
style="left: 0%; transform: translateX(-50%); background: rgba(0, 0, 0, 0);"
tabindex="0"
/>
</div>
</div>
</div>
<div
@ -22742,7 +22742,7 @@ exports[`renders components/form/demo/validate-other.tsx extend context correctl
style="position: relative;"
>
<div
style="position: absolute; left: 0px; top: 0px; z-index: 1;"
style="position: absolute; left: 0%; top: 100%; z-index: 1; transform: translate(-50%, -50%);"
>
<div
class="ant-color-picker-handler"
@ -22762,46 +22762,46 @@ exports[`renders components/form/demo/validate-other.tsx extend context correctl
class="ant-color-picker-slider-group"
>
<div
class="ant-color-picker-slider ant-color-picker-slider-hue"
class="ant-slider ant-color-picker-slider ant-slider-horizontal"
>
<div
class="ant-color-picker-palette"
style="position: relative;"
>
class="ant-slider-rail ant-color-picker-slider-rail"
/>
<div
style="position: absolute; left: 0px; top: 0px; z-index: 1;"
>
class="ant-slider-step"
/>
<div
class="ant-color-picker-handler ant-color-picker-handler-sm"
style="background-color: rgb(255, 0, 0);"
aria-disabled="false"
aria-orientation="horizontal"
aria-valuemax="359"
aria-valuemin="0"
aria-valuenow="0"
class="ant-slider-handle ant-slider-handle-1 ant-color-picker-slider-handle"
role="slider"
style="left: 0%; transform: translateX(-50%); background: rgb(255, 0, 0);"
tabindex="0"
/>
</div>
<div
class="ant-color-picker-gradient"
style="position: absolute; inset: 0;"
/>
</div>
</div>
<div
class="ant-color-picker-slider ant-color-picker-slider-alpha"
class="ant-slider ant-color-picker-slider ant-slider-horizontal"
>
<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: rgba(0, 0, 0, 0);"
class="ant-slider-rail ant-color-picker-slider-rail"
/>
</div>
<div
class="ant-color-picker-gradient"
style="position: absolute; inset: 0;"
class="ant-slider-step"
/>
<div
aria-disabled="false"
aria-orientation="horizontal"
aria-valuemax="100"
aria-valuemin="0"
aria-valuenow="0"
class="ant-slider-handle ant-slider-handle-1 ant-color-picker-slider-handle"
role="slider"
style="left: 0%; transform: translateX(-50%); background: rgba(0, 0, 0, 0);"
tabindex="0"
/>
</div>
</div>
</div>
<div

View File

@ -34,7 +34,7 @@ export interface ComponentToken {
* @desc
* @descEN Label height
*/
labelHeight: number;
labelHeight: number | string;
/**
* @desc
* @descEN Label colon margin-inline-start

View File

@ -3203,7 +3203,7 @@ exports[`renders components/input-number/demo/out-of-range.tsx extend context co
exports[`renders components/input-number/demo/out-of-range.tsx extend context correctly 2`] = `[]`;
exports[`renders components/input-number/demo/prefix.tsx extend context correctly 1`] = `
exports[`renders components/input-number/demo/presuffix.tsx extend context correctly 1`] = `
Array [
<div
class="ant-input-number-affix-wrapper ant-input-number-outlined"
@ -3490,10 +3490,95 @@ Array [
</div>
</div>
</div>,
<br />,
<br />,
<div
class="ant-input-number-affix-wrapper ant-input-number-outlined"
style="width: 100%;"
>
<div
class="ant-input-number"
>
<div
class="ant-input-number-handler-wrap"
>
<span
aria-disabled="false"
aria-label="Increase Value"
class="ant-input-number-handler ant-input-number-handler-up"
role="button"
unselectable="on"
>
<span
aria-label="up"
class="anticon anticon-up ant-input-number-handler-up-inner"
role="img"
>
<svg
aria-hidden="true"
data-icon="up"
fill="currentColor"
focusable="false"
height="1em"
viewBox="64 64 896 896"
width="1em"
>
<path
d="M890.5 755.3L537.9 269.2c-12.8-17.6-39-17.6-51.7 0L133.5 755.3A8 8 0 00140 768h75c5.1 0 9.9-2.5 12.9-6.6L512 369.8l284.1 391.6c3 4.1 7.8 6.6 12.9 6.6h75c6.5 0 10.3-7.4 6.5-12.7z"
/>
</svg>
</span>
</span>
<span
aria-disabled="false"
aria-label="Decrease Value"
class="ant-input-number-handler ant-input-number-handler-down"
role="button"
unselectable="on"
>
<span
aria-label="down"
class="anticon anticon-down ant-input-number-handler-down-inner"
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-input-number-input-wrap"
>
<input
autocomplete="off"
class="ant-input-number-input"
role="spinbutton"
step="1"
value=""
/>
</div>
</div>
<span
class="ant-input-number-suffix"
>
RMB
</span>
</div>,
]
`;
exports[`renders components/input-number/demo/prefix.tsx extend context correctly 2`] = `[]`;
exports[`renders components/input-number/demo/presuffix.tsx extend context correctly 2`] = `[]`;
exports[`renders components/input-number/demo/render-panel.tsx extend context correctly 1`] = `
<div

View File

@ -2911,7 +2911,7 @@ exports[`renders components/input-number/demo/out-of-range.tsx correctly 1`] = `
</div>
`;
exports[`renders components/input-number/demo/prefix.tsx correctly 1`] = `
exports[`renders components/input-number/demo/presuffix.tsx correctly 1`] = `
Array [
<div
class="ant-input-number-affix-wrapper ant-input-number-outlined"
@ -3198,6 +3198,91 @@ Array [
</div>
</div>
</div>,
<br />,
<br />,
<div
class="ant-input-number-affix-wrapper ant-input-number-outlined"
style="width:100%"
>
<div
class="ant-input-number"
>
<div
class="ant-input-number-handler-wrap"
>
<span
aria-disabled="false"
aria-label="Increase Value"
class="ant-input-number-handler ant-input-number-handler-up"
role="button"
unselectable="on"
>
<span
aria-label="up"
class="anticon anticon-up ant-input-number-handler-up-inner"
role="img"
>
<svg
aria-hidden="true"
data-icon="up"
fill="currentColor"
focusable="false"
height="1em"
viewBox="64 64 896 896"
width="1em"
>
<path
d="M890.5 755.3L537.9 269.2c-12.8-17.6-39-17.6-51.7 0L133.5 755.3A8 8 0 00140 768h75c5.1 0 9.9-2.5 12.9-6.6L512 369.8l284.1 391.6c3 4.1 7.8 6.6 12.9 6.6h75c6.5 0 10.3-7.4 6.5-12.7z"
/>
</svg>
</span>
</span>
<span
aria-disabled="false"
aria-label="Decrease Value"
class="ant-input-number-handler ant-input-number-handler-down"
role="button"
unselectable="on"
>
<span
aria-label="down"
class="anticon anticon-down ant-input-number-handler-down-inner"
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-input-number-input-wrap"
>
<input
autocomplete="off"
class="ant-input-number-input"
role="spinbutton"
step="1"
value=""
/>
</div>
</div>
<span
class="ant-input-number-suffix"
>
RMB
</span>
</div>,
]
`;

View File

@ -0,0 +1,19 @@
import React from 'react';
import InputNumber from '..';
import { fireEvent, render } from '../../../tests/utils';
describe('suffix', () => {
it('should support suffix prop', () => {
const { container } = render(<InputNumber suffix={<i>hello</i>} />);
expect(container.querySelector('.ant-input-number-suffix')).toBeInTheDocument();
});
it('should trigger focus when suffix is clicked', () => {
const { container } = render(<InputNumber suffix={<i>antd</i>} />);
const mockFocus = jest.spyOn(container.querySelector('input')!, 'focus');
fireEvent.click(container.querySelector('i')!);
expect(mockFocus).toHaveBeenCalled();
});
});

View File

@ -1,7 +0,0 @@
## zh-CN
在输入框上添加前缀图标。
## en-US
Add a prefix inside input.

View File

@ -0,0 +1,7 @@
## zh-CN
在输入框上添加前缀或后缀图标。
## en-US
Add a prefix or suffix inside input.

View File

@ -11,6 +11,9 @@ const App: React.FC = () => (
<br />
<br />
<InputNumber prefix="¥" disabled style={{ width: '100%' }} />
<br />
<br />
<InputNumber suffix="RMB" style={{ width: '100%' }} />
</>
);

View File

@ -27,7 +27,7 @@ When a numeric value needs to be provided.
<code src="./demo/variant.tsx" version="5.13.0">Variants</code>
<code src="./demo/filled-debug.tsx" debug>Filled Debug</code>
<code src="./demo/out-of-range.tsx">Out of range</code>
<code src="./demo/prefix.tsx">Prefix</code>
<code src="./demo/presuffix.tsx">Prefix / Suffix</code>
<code src="./demo/status.tsx">Status</code>
<code src="./demo/controls.tsx" debug>Icon</code>
<code src="./demo/render-panel.tsx" debug>_InternalPanelDoNotUseOrYouWillBeFired</code>
@ -58,6 +58,7 @@ Common props ref[Common props](/docs/react/common-props)
| readOnly | If readonly the input | boolean | false | - |
| status | Set validation status | 'error' \| 'warning' | - | 4.19.0 |
| prefix | The prefix icon for the Input | ReactNode | - | 4.17.0 |
| suffix | The suffix icon for the Input | ReactNode | - | 5.20.0 |
| size | The height of input box | `large` \| `middle` \| `small` | - | - |
| step | The number to which the current value is increased or decreased. It can be an integer or decimal | number \| string | 1 | - |
| stringMode | Set value as string to support high precision decimals. Will return string value by `onChange` | boolean | false | 4.13.0 |

View File

@ -27,6 +27,7 @@ export interface InputNumberProps<T extends ValueType = ValueType>
addonBefore?: React.ReactNode;
addonAfter?: React.ReactNode;
prefix?: React.ReactNode;
suffix?: React.ReactNode;
size?: SizeType;
disabled?: boolean;
/** @deprecated Use `variant` instead. */
@ -66,6 +67,7 @@ const InputNumber = React.forwardRef<HTMLInputElement, InputNumberProps>((props,
addonBefore,
addonAfter,
prefix,
suffix,
bordered,
readOnly,
status: customStatus,
@ -141,7 +143,7 @@ const InputNumber = React.forwardRef<HTMLInputElement, InputNumberProps>((props,
readOnly={readOnly}
controls={controlsTemp}
prefix={prefix}
suffix={suffixNode}
suffix={suffixNode || suffix}
addonBefore={
addonBefore && (
<ContextIsolator form space>

View File

@ -28,7 +28,7 @@ demo:
<code src="./demo/variant.tsx" version="5.13.0">形态变体</code>
<code src="./demo/filled-debug.tsx" debug>Filled Debug</code>
<code src="./demo/out-of-range.tsx">超出边界</code>
<code src="./demo/prefix.tsx">前缀</code>
<code src="./demo/presuffix.tsx">缀/后</code>
<code src="./demo/status.tsx">自定义状态</code>
<code src="./demo/controls.tsx" debug>图标按钮</code>
<code src="./demo/render-panel.tsx" debug>_InternalPanelDoNotUseOrYouWillBeFired</code>
@ -59,6 +59,7 @@ demo:
| readOnly | 只读 | boolean | false | - |
| status | 设置校验状态 | 'error' \| 'warning' | - | 4.19.0 |
| prefix | 带有前缀图标的 input | ReactNode | - | 4.17.0 |
| suffix | 带有后缀图标的 input | ReactNode | - | 5.20.0 |
| size | 输入框大小 | `large` \| `middle` \| `small` | - | - |
| step | 每次改变步数,可以为小数 | number \| string | 1 | - |
| stringMode | 字符值模式,开启后支持高精度小数。同时 `onChange` 将返回 string 类型 | boolean | false | 4.13.0 |

View File

@ -61,6 +61,7 @@ const genInputNumberStyles: GenerateStyle<InputNumberToken> = (token: InputNumbe
colorTextDescription,
motionDurationMid,
handleHoverColor,
handleOpacity,
paddingInline,
paddingBlock,
handleBg,
@ -69,7 +70,6 @@ const genInputNumberStyles: GenerateStyle<InputNumberToken> = (token: InputNumbe
borderRadiusSM,
borderRadiusLG,
controlWidth,
handleOpacity,
handleBorderColor,
filledHandleBg,
lineHeightLG,
@ -236,31 +236,32 @@ const genInputNumberStyles: GenerateStyle<InputNumberToken> = (token: InputNumbe
},
},
},
[`&:hover ${componentCls}-handler-wrap, &-focused ${componentCls}-handler-wrap`]: {
width: token.handleWidth,
opacity: 1,
},
},
},
// Handler
{
[componentCls]: {
[`&:hover ${componentCls}-handler-wrap, &-focused ${componentCls}-handler-wrap`]: {
opacity: 1,
},
[`${componentCls}-handler-wrap`]: {
position: 'absolute',
insetBlockStart: 0,
insetInlineEnd: 0,
width: token.handleWidth,
width: 0,
opacity: handleOpacity,
height: '100%',
borderStartStartRadius: 0,
borderStartEndRadius: borderRadius,
borderEndEndRadius: borderRadius,
borderEndStartRadius: 0,
opacity: handleOpacity,
display: 'flex',
flexDirection: 'column',
alignItems: 'stretch',
transition: `opacity ${motionDurationMid} linear ${motionDurationMid}`,
transition: `all ${motionDurationMid}`,
overflow: 'hidden',
// Fix input number inside Menu makes icon too large
// We arise the selector priority by nest selector here
@ -370,6 +371,7 @@ const genAffixWrapperStyles: GenerateStyle<InputNumberToken> = (token: InputNumb
paddingInlineSM,
paddingBlockLG,
paddingBlockSM,
motionDurationMid,
} = token;
return {
@ -382,6 +384,7 @@ const genAffixWrapperStyles: GenerateStyle<InputNumberToken> = (token: InputNumb
// or number handler will cover form status
position: 'relative',
display: 'inline-flex',
alignItems: 'center',
width: controlWidth,
padding: 0,
paddingInlineStart: paddingInline,
@ -438,6 +441,7 @@ const genAffixWrapperStyles: GenerateStyle<InputNumberToken> = (token: InputNumb
},
[componentCls]: {
position: 'static',
color: 'inherit',
'&-prefix, &-suffix': {
@ -452,15 +456,22 @@ const genAffixWrapperStyles: GenerateStyle<InputNumberToken> = (token: InputNumb
},
'&-suffix': {
position: 'absolute',
insetBlockStart: 0,
insetInlineEnd: 0,
zIndex: 1,
height: '100%',
marginInlineEnd: paddingInline,
marginInlineStart: inputAffixPadding,
transition: `margin ${motionDurationMid}`,
},
},
[`&:hover ${componentCls}-handler-wrap, &-focused ${componentCls}-handler-wrap`]: {
width: token.handleWidth,
opacity: 1,
},
[`&:hover ${componentCls}-suffix`]: {
marginInlineEnd: token.calc(token.handleWidth).add(paddingInline).equal(),
},
},
};
};

View File

@ -124,10 +124,12 @@ describe('Input.Search', () => {
const { container } = render(<Search defaultValue="search text" onSearch={onSearch} />);
fireEvent.compositionStart(container.querySelector('input')!);
fireEvent.keyDown(container.querySelector('input')!, { key: 'Enter', keyCode: 13 });
fireEvent.keyUp(container.querySelector('input')!, { key: 'Enter', keyCode: 13 });
expect(onSearch).not.toHaveBeenCalled();
fireEvent.compositionEnd(container.querySelector('input')!);
fireEvent.keyDown(container.querySelector('input')!, { key: 'Enter', keyCode: 13 });
fireEvent.keyUp(container.querySelector('input')!, { key: 'Enter', keyCode: 13 });
expect(onSearch).toHaveBeenCalledTimes(1);
expect(onSearch).toHaveBeenCalledWith('search text', expect.anything(), { source: 'input' });
});

View File

@ -71,6 +71,7 @@ Common props ref[Common props](/docs/react/common-props)
| variant | Variants of Input | `outlined` \| `borderless` \| `filled` | `outlined` | 5.13.0 |
| onChange | Callback when user input | function(e) | - | |
| onPressEnter | The callback function that is triggered when Enter key is pressed | function(e) | - | |
| onClear | Callback when click the clear button | () => void | - | 5.20.0 |
> When `Input` is used in a `Form.Item` context, if the `Form.Item` has the `id` props defined then `value`, `defaultValue`, and `id` props of `Input` are automatically set.

View File

@ -72,6 +72,7 @@ demo:
| variant | 形态变体 | `outlined` \| `borderless` \| `filled` | `outlined` | 5.13.0 |
| onChange | 输入框内容变化时的回调 | function(e) | - | |
| onPressEnter | 按下回车的回调 | function(e) | - | |
| onClear | 按下清除按钮的回调 | () => void | - | 5.20.0 |
> 如果 `Input``Form.Item` 内,并且 `Form.Item` 设置了 `id` 属性,则 `value` `defaultValue``id` 属性会被自动设置。

View File

@ -58,7 +58,7 @@ export interface ComponentToken {
* @desc
* @descEN Height of sider trigger
*/
triggerHeight: number;
triggerHeight: number | string;
/**
* @desc
* @descEN Background Color of sider trigger

View File

@ -11,7 +11,7 @@ export interface ComponentToken {
* @desc
* @descEN Width of content
*/
contentWidth: number;
contentWidth: number | string;
/**
* @desc
* @descEN Padding of large item
@ -66,7 +66,7 @@ export interface ComponentToken {
interface ListToken extends FullToken<'List'> {
listBorderedCls: string;
minHeight: number;
minHeight: number | string;
}
const genBorderedStyle = (token: ListToken): CSSObject => {

View File

@ -144,6 +144,9 @@ const localeValues: Locale = {
},
ColorPicker: {
presetEmpty: 'Boşdur',
transparent: 'Şəffaf',
singleColor: 'Tək rəng',
gradientColor: 'Gradient rəng',
},
};

View File

@ -143,6 +143,9 @@ const localeValues: Locale = {
},
ColorPicker: {
presetEmpty: 'Empty',
transparent: 'Transparent',
singleColor: 'Single',
gradientColor: 'Gradient',
},
};

View File

@ -140,6 +140,9 @@ const localeValues: Locale = {
},
ColorPicker: {
presetEmpty: 'Hustu',
transparent: 'Gardena',
singleColor: 'Kolore bakarra',
gradientColor: 'Gradiente kolorea',
},
};

View File

@ -143,6 +143,9 @@ const localeValues: Locale = {
},
ColorPicker: {
presetEmpty: 'Kosong',
transparent: 'Transparan',
singleColor: 'Warna tunggal',
gradientColor: 'Warna gradien',
},
};

View File

@ -56,6 +56,9 @@ export interface Locale {
};
ColorPicker?: {
presetEmpty: string;
transparent: string;
singleColor: string;
gradientColor: string;
};
}

View File

@ -143,6 +143,9 @@ const localeValues: Locale = {
},
ColorPicker: {
presetEmpty: '空の',
transparent: '透明',
singleColor: '単色',
gradientColor: 'グラデーション',
},
};

View File

@ -140,6 +140,9 @@ const localeValues: Locale = {
},
ColorPicker: {
presetEmpty: '미정',
transparent: '투명',
singleColor: '단색',
gradientColor: '그라데이션',
},
};

View File

@ -140,6 +140,9 @@ const localeValues: Locale = {
},
ColorPicker: {
presetEmpty: 'Tuščia',
transparent: 'Permatomas',
singleColor: 'Vieno spalvos',
gradientColor: 'Gradientas',
},
};

View File

@ -141,6 +141,9 @@ const localeValues: Locale = {
},
ColorPicker: {
presetEmpty: 'Tiada',
transparent: 'Tidak tembus cahaya',
singleColor: 'Warna tunggal',
gradientColor: 'Warna gradien',
},
};

View File

@ -142,6 +142,9 @@ const localeValues: Locale = {
},
ColorPicker: {
presetEmpty: 'अहिलेसम्म कुनै पनि छैन',
transparent: 'पारदर्शी',
singleColor: 'एक रंग',
gradientColor: 'ग्रेडिएण्ट',
},
};

View File

@ -143,6 +143,9 @@ const localeValues: Locale = {
},
ColorPicker: {
presetEmpty: 'ไม่มีข้อมูล',
transparent: 'โปร่งใส',
singleColor: 'สีเดียว',
gradientColor: 'สีไล่ระดับ',
},
};

View File

@ -144,6 +144,9 @@ const localeValues: Locale = {
},
ColorPicker: {
presetEmpty: '暂无',
transparent: '无色',
singleColor: '单色',
gradientColor: '渐变色',
},
};

View File

@ -74,6 +74,7 @@ Common props ref[Common props](/docs/react/common-props)
| variant | Variants of Input | `outlined` \| `borderless` \| `filled` | `outlined` | 5.13.0 |
| onBlur | Trigger when mentions lose focus | () => void | - | |
| onChange | Trigger when value changed | (text: string) => void | - | |
| onClear | Callback when click the clear button | () => void | - | 5.20.0 |
| onFocus | Trigger when mentions get focus | () => void | - | |
| onResize | The callback function that is triggered when textarea resize | function({ width, height }) | - | |
| onSearch | Trigger when prefix hit | (text: string, prefix: string) => void | - | |

View File

@ -75,6 +75,7 @@ return (
| variant | 形态变体 | `outlined` \| `borderless` \| `filled` | `outlined` | 5.13.0 |
| onBlur | 失去焦点时触发 | () => void | - | |
| onChange | 值改变时触发 | (text: string) => void | - | |
| onClear | 按下清除按钮的回调 | () => void | - | 5.20.0 |
| onFocus | 获得焦点时触发 | () => void | - | |
| onResize | resize 回调 | function({ width, height }) | - | |
| onSearch | 搜索时触发 | (text: string, prefix: string) => void | - | |

Some files were not shown because too many files have changed in this diff Show More