feat: ColorPicker support gradient color (#50050)

* refactor: support type update

* chore: update clear style

* chore: bump color picker

* chore: use slider

* chore: bump color picker

* chore: range slider

* chore: layout

* chore: useModeColor

* chore: simplify

* chore: bump color picker

* refactor: event

* chore: tmp lock check

* chore: of it

* chore: update ts def

* chore: update ts def

* chore: remove useless ts

* chore: linear

* chore: adjust style

* chore: rm useless code

* chore: fill color

* chore: basic linear

* chore: support toStr

* chore: limit minCount

* chore: use cache

* chore: drag support:

* chore: yes

* chore: update demo

* chore: useLayoutEffect instead

* chore: fix click to add point

* chore: add smmoth

* chore: support of locale

* chore: add locale

* chore: fix lint

* chore: adjust style

* chore: fix lint

* chore: fix style

* test: fix test case

* chore: fix popover

* test: fix test case

* chore: fix test

* test: clean up

* chore: fix lint

* chore: fix lint

* chore: fix lint

* test: coverage

* test: coverage

* test: coverage

* test: coverage

* test: coverage

* test: coverage

* chore: fix docs

* docs: update demo desc

* chore: enhance hover range

* fix: delete not working

* chore: fix lint

* test: coverage

* test: coverage

* chore: clean up

* chore: adjust

* chore: highlight

* chore: adjust style

* chore: fix lint

* chore: update demo

* chore: memo perf

* refactor: up to down colors

* test: update snapshot
This commit is contained in:
二货爱吃白萝卜 2024-07-29 16:38:50 +08:00 committed by GitHub
parent aed4665047
commit 832cffcdf9
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
80 changed files with 3889 additions and 1557 deletions

View File

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

View File

@ -40,7 +40,7 @@ import RadiusPicker from './RadiusPicker';
import type { THEME } from './ThemePicker'; import type { THEME } from './ThemePicker';
import ThemePicker 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; const { Header, Content, Sider } = Layout;

View File

@ -18,13 +18,8 @@ export type CustomComponent<P = AnyObject> = React.ComponentType<P> | string;
* ``` * ```
* @since 5.13.0 * @since 5.13.0
*/ */
export type GetProps<T extends React.ComponentType<any> | object> = T extends React.ComponentType< export type GetProps<T extends React.ComponentType<any> | object> =
infer P T extends React.ComponentType<infer P> ? P : T extends object ? T : never;
>
? P
: T extends object
? T
: never;
/** /**
* Get component props by component name * 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> : T extends React.ComponentType<infer P>
? ExtractRefAttributesRef<P> ? ExtractRefAttributesRef<P>
: never; : 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 { Keyframes, unit } from '@ant-design/cssinjs';
import { resetComponent } from '../../style'; import { resetComponent } from '../../style';
import type { import type { FullToken, GenerateStyle, GenStyleFn, GetDefaultToken } from '../../theme/internal';
FullToken,
GenerateStyle,
GenStyleFn,
GetDefaultToken,
} from '../../theme/internal';
import { genPresetColor, genStyleHooks, mergeToken } from '../../theme/internal'; import { genPresetColor, genStyleHooks, mergeToken } from '../../theme/internal';
/** Component only token. Which will handle additional calculation of alias token */ /** Component only token. Which will handle additional calculation of alias token */

View File

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

View File

@ -1,4 +1,4 @@
import React, { useContext, useMemo, useRef } from 'react'; import React, { useContext, useMemo } from 'react';
import classNames from 'classnames'; import classNames from 'classnames';
import useMergedState from 'rc-util/lib/hooks/useMergedState'; 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 { FormItemInputContext } from '../form/context';
import type { PopoverProps } from '../popover'; import type { PopoverProps } from '../popover';
import Popover from '../popover'; import Popover from '../popover';
import type { AggregationColor } from './color'; import { AggregationColor } from './color';
import type { ColorPickerPanelProps } from './ColorPickerPanel'; import type { ColorPickerPanelProps } from './ColorPickerPanel';
import ColorPickerPanel from './ColorPickerPanel'; import ColorPickerPanel from './ColorPickerPanel';
import ColorTrigger from './components/ColorTrigger'; import ColorTrigger from './components/ColorTrigger';
import useColorState from './hooks/useColorState'; import useModeColor from './hooks/useModeColor';
import type { ColorPickerBaseProps, ColorPickerProps, TriggerPlacement } from './interface'; import type { ColorPickerProps, ModeType, TriggerPlacement } from './interface';
import useStyle from './style'; import useStyle from './style';
import { genAlphaColor, generateColor, getAlphaColor } from './util'; import { genAlphaColor, generateColor, getColorAlpha } from './util';
type CompoundedComponent = React.FC<ColorPickerProps> & { type CompoundedComponent = React.FC<ColorPickerProps> & {
_InternalPanelDoNotUseOrYouWillBeFired: typeof PurePanel; _InternalPanelDoNotUseOrYouWillBeFired: typeof PurePanel;
@ -29,6 +29,7 @@ type CompoundedComponent = React.FC<ColorPickerProps> & {
const ColorPicker: CompoundedComponent = (props) => { const ColorPicker: CompoundedComponent = (props) => {
const { const {
mode,
value, value,
defaultValue, defaultValue,
format, format,
@ -65,10 +66,6 @@ const ColorPicker: CompoundedComponent = (props) => {
const contextDisabled = useContext(DisabledContext); const contextDisabled = useContext(DisabledContext);
const mergedDisabled = disabled ?? contextDisabled; const mergedDisabled = disabled ?? contextDisabled;
const [colorValue, setColorValue, prevValue] = useColorState('', {
value,
defaultValue,
});
const [popupOpen, setPopupOpen] = useMergedState(false, { const [popupOpen, setPopupOpen] = useMergedState(false, {
value: open, value: open,
postState: (openData) => !mergedDisabled && openData, postState: (openData) => !mergedDisabled && openData,
@ -82,9 +79,91 @@ const ColorPicker: CompoundedComponent = (props) => {
const prefixCls = getPrefixCls('color-picker', customizePrefixCls); 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); const { status: contextStatus } = React.useContext(FormItemInputContext);
// ===================== Style ===================== // ===================== Style =====================
@ -106,8 +185,6 @@ const ColorPicker: CompoundedComponent = (props) => {
); );
const mergedPopupCls = classNames(prefixCls, mergedRootCls); const mergedPopupCls = classNames(prefixCls, mergedRootCls);
const popupAllowCloseRef = useRef(true);
// ===================== Warning ====================== // ===================== Warning ======================
if (process.env.NODE_ENV !== 'production') { if (process.env.NODE_ENV !== 'production') {
const warning = devUseWarning('ColorPicker'); const warning = devUseWarning('ColorPicker');
@ -119,48 +196,6 @@ const ColorPicker: CompoundedComponent = (props) => {
); );
} }
const handleChange: ColorPickerPanelProps['onChange'] = (data, type, pickColor) => {
let color: AggregationColor = generateColor(data as AggregationColor);
// 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 = { const popoverProps: PopoverProps = {
open: popupOpen, open: popupOpen,
trigger, trigger,
@ -172,19 +207,6 @@ const ColorPicker: CompoundedComponent = (props) => {
destroyTooltipOnHide, 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 }; const mergedStyle: React.CSSProperties = { ...colorPicker?.style, ...style };
// ============================ zIndex ============================ // ============================ zIndex ============================
@ -194,17 +216,32 @@ const ColorPicker: CompoundedComponent = (props) => {
style={styles?.popup} style={styles?.popup}
overlayInnerStyle={styles?.popupOverlayInner} overlayInnerStyle={styles?.popupOverlayInner}
onOpenChange={(visible) => { onOpenChange={(visible) => {
if (popupAllowCloseRef.current && !mergedDisabled) { if (!visible || !mergedDisabled) {
setPopupOpen(visible); setPopupOpen(visible);
} }
}} }}
content={ content={
<ContextIsolator form> <ContextIsolator form>
<ColorPickerPanel <ColorPickerPanel
{...colorBaseProps} mode={modeState}
onChange={handleChange} onModeChange={onInternalModeChange}
onChangeComplete={handleChangeComplete} modeOptions={modeOptions}
onClear={handleClear} 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> </ContextIsolator>
} }
@ -213,6 +250,7 @@ const ColorPicker: CompoundedComponent = (props) => {
> >
{children || ( {children || (
<ColorTrigger <ColorTrigger
activeIndex={popupOpen ? activeIndex : -1}
open={popupOpen} open={popupOpen}
className={mergedCls} className={mergedCls}
style={mergedStyle} style={mergedStyle}
@ -221,7 +259,7 @@ const ColorPicker: CompoundedComponent = (props) => {
showText={showText} showText={showText}
format={formatValue} format={formatValue}
{...rest} {...rest}
color={colorValue} color={mergedColor}
/> />
)} )}
</Popover>, </Popover>,

View File

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

View File

@ -206,6 +206,93 @@ exports[`renders components/color-picker/demo/format.tsx correctly 1`] = `
</div> </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`] = ` exports[`renders components/color-picker/demo/panel-render.tsx correctly 1`] = `
<div <div
class="ant-space ant-space-vertical ant-space-gap-row-small ant-space-gap-col-small" class="ant-space ant-space-vertical ant-space-gap-row-small ant-space-gap-col-small"

View File

@ -59,7 +59,7 @@ exports[`ColorPicker Should panelRender work 1`] = `
style="position: relative;" style="position: relative;"
> >
<div <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 <div
class="ant-color-picker-handler" class="ant-color-picker-handler"
@ -79,46 +79,46 @@ exports[`ColorPicker Should panelRender work 1`] = `
class="ant-color-picker-slider-group" class="ant-color-picker-slider-group"
> >
<div <div
class="ant-color-picker-slider ant-color-picker-slider-hue" class="ant-slider ant-color-picker-slider ant-slider-horizontal"
> >
<div <div
class="ant-color-picker-palette" class="ant-slider-rail ant-color-picker-slider-rail"
style="position: relative;" />
>
<div <div
style="position: absolute; left: -50px; top: -16.666666666666668px; z-index: 1;" class="ant-slider-step"
> />
<div <div
class="ant-color-picker-handler ant-color-picker-handler-sm" aria-disabled="false"
style="background-color: rgb(255, 0, 0);" 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>
<div <div
class="ant-color-picker-gradient" class="ant-slider ant-color-picker-slider ant-slider-horizontal"
style="position: absolute; inset: 0;"
/>
</div>
</div>
<div
class="ant-color-picker-slider ant-color-picker-slider-alpha"
> >
<div <div
class="ant-color-picker-palette" class="ant-slider-rail ant-color-picker-slider-rail"
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);"
/> />
</div>
<div <div
class="ant-color-picker-gradient" class="ant-slider-step"
style="position: absolute; inset: 0;" />
<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> </div>
<div <div
@ -343,7 +343,7 @@ exports[`ColorPicker Should panelRender work 2`] = `
style="position: relative;" style="position: relative;"
> >
<div <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 <div
class="ant-color-picker-handler" class="ant-color-picker-handler"
@ -363,46 +363,46 @@ exports[`ColorPicker Should panelRender work 2`] = `
class="ant-color-picker-slider-group" class="ant-color-picker-slider-group"
> >
<div <div
class="ant-color-picker-slider ant-color-picker-slider-hue" class="ant-slider ant-color-picker-slider ant-slider-horizontal"
> >
<div <div
class="ant-color-picker-palette" class="ant-slider-rail ant-color-picker-slider-rail"
style="position: relative;" />
>
<div <div
style="position: absolute; left: -50px; top: -16.666666666666668px; z-index: 1;" class="ant-slider-step"
> />
<div <div
class="ant-color-picker-handler ant-color-picker-handler-sm" aria-disabled="false"
style="background-color: rgb(255, 0, 0);" 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>
<div <div
class="ant-color-picker-gradient" class="ant-slider ant-color-picker-slider ant-slider-horizontal"
style="position: absolute; inset: 0;"
/>
</div>
</div>
<div
class="ant-color-picker-slider ant-color-picker-slider-alpha"
> >
<div <div
class="ant-color-picker-palette" class="ant-slider-rail ant-color-picker-slider-rail"
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);"
/> />
</div>
<div <div
class="ant-color-picker-gradient" class="ant-slider-step"
style="position: absolute; inset: 0;" />
<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> </div>
<div <div

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 ConfigProvider from '../../config-provider';
import Form from '../../form'; import Form from '../../form';
import theme from '../../theme'; import theme from '../../theme';
import type { AggregationColor } from '../color'; import { AggregationColor } from '../color';
import ColorPicker from '../ColorPicker'; import ColorPicker from '../ColorPicker';
import type { ColorPickerProps, ColorValueType } from '../interface'; import type { ColorPickerProps, ColorValueType } from '../interface';
import { generateColor } from '../util'; import { generateColor } from '../util';
@ -25,6 +25,11 @@ function doMouseMove(
pageX: start, pageX: start,
pageY: start, pageY: start,
}); });
Object.defineProperties(mouseDown, {
pageX: { get: () => start },
pageY: { get: () => start },
});
fireEvent(container.getElementsByClassName(element)[0], mouseDown); fireEvent(container.getElementsByClassName(element)[0], mouseDown);
// Drag // Drag
const mouseMove: any = new Event('mousemove'); const mouseMove: any = new Event('mousemove');
@ -340,7 +345,7 @@ describe('ColorPicker', () => {
}); });
it('Should fix hover boundary issues', async () => { it('Should fix hover boundary issues', async () => {
spyElementPrototypes(HTMLElement, { const spyRect = spyElementPrototypes(HTMLElement, {
getBoundingClientRect: () => ({ getBoundingClientRect: () => ({
x: 0, x: 0,
y: 100, y: 100,
@ -356,6 +361,8 @@ describe('ColorPicker', () => {
fireEvent.mouseLeave(container.querySelector('.ant-color-picker-trigger')!); fireEvent.mouseLeave(container.querySelector('.ant-color-picker-trigger')!);
await waitFakeTimer(); await waitFakeTimer();
expect(container.querySelector('.ant-popover-hidden')).toBeTruthy(); expect(container.querySelector('.ant-popover-hidden')).toBeTruthy();
spyRect.mockRestore();
}); });
it('Should work at dark mode', async () => { it('Should work at dark mode', async () => {
@ -385,6 +392,12 @@ describe('ColorPicker', () => {
expect(targetEle?.innerHTML).toBe('#1677ff'); 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 () => { it('Should showText work', async () => {
const { container } = render(<ColorPicker defaultValue="#1677ff" open showText />); const { container } = render(<ColorPicker defaultValue="#1677ff" open showText />);
const targetEle = container.querySelector('.ant-color-picker-trigger-text'); const targetEle = container.querySelector('.ant-color-picker-trigger-text');
@ -448,7 +461,7 @@ describe('ColorPicker', () => {
}); });
it('Should null work as expect', async () => { it('Should null work as expect', async () => {
spyElementPrototypes(HTMLElement, { const spyRect = spyElementPrototypes(HTMLElement, {
getBoundingClientRect: () => ({ getBoundingClientRect: () => ({
x: 0, x: 0,
y: 100, y: 100,
@ -456,7 +469,8 @@ describe('ColorPicker', () => {
height: 100, height: 100,
}), }),
}); });
const { container } = render(<ColorPicker value={null} open />);
const { container } = render(<ColorPicker defaultValue={null} open />);
expect( expect(
container.querySelector('.ant-color-picker-alpha-input input')?.getAttribute('value'), container.querySelector('.ant-color-picker-alpha-input input')?.getAttribute('value'),
).toEqual('0%'); ).toEqual('0%');
@ -467,6 +481,8 @@ describe('ColorPicker', () => {
expect( expect(
container.querySelector('.ant-color-picker-alpha-input input')?.getAttribute('value'), container.querySelector('.ant-color-picker-alpha-input input')?.getAttribute('value'),
).toEqual('100%'); ).toEqual('100%');
spyRect.mockRestore();
}); });
it('should support valid in form', async () => { it('should support valid in form', async () => {
@ -501,17 +517,37 @@ describe('ColorPicker', () => {
}); });
it('Should onChangeComplete work', async () => { it('Should onChangeComplete work', async () => {
const spyRect = spyElementPrototypes(HTMLElement, {
getBoundingClientRect: () => ({
x: 0,
y: 100,
width: 100,
height: 100,
}),
});
const handleChangeComplete = jest.fn(); const handleChangeComplete = jest.fn();
const { container } = render( const { container } = render(
<ColorPicker open onChangeComplete={handleChangeComplete} allowClear />, <ColorPicker open onChangeComplete={handleChangeComplete} allowClear />,
); );
// Move
doMouseMove(container, 0, 999); 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')!, { fireEvent.change(container.querySelector('.ant-color-picker-hex-input input')!, {
target: { value: '#273B57' }, target: { value: '#273B57' },
}); });
expect(handleChangeComplete).toHaveBeenCalledTimes(3); expect(handleChangeComplete).toHaveBeenCalledTimes(3);
spyRect.mockRestore();
}); });
it('Should disabledAlpha work', async () => { it('Should disabledAlpha work', async () => {
@ -522,7 +558,7 @@ describe('ColorPicker', () => {
}); });
it('Should disabledAlpha work with value', async () => { it('Should disabledAlpha work with value', async () => {
spyElementPrototypes(HTMLElement, { const spyRect = spyElementPrototypes(HTMLElement, {
getBoundingClientRect: () => ({ getBoundingClientRect: () => ({
x: 0, x: 0,
y: 100, y: 100,
@ -542,10 +578,12 @@ describe('ColorPicker', () => {
onChangeComplete={setChangedValue} onChangeComplete={setChangedValue}
> >
<div className="color-value"> <div className="color-value">
{typeof value === 'string' ? value : value?.toHexString()} {value instanceof AggregationColor ? value.toHexString() : String(value)}
</div> </div>
<div className="color-value-changed"> <div className="color-value-changed">
{typeof changedValue === 'string' ? changedValue : changedValue?.toHexString()} {changedValue instanceof AggregationColor
? changedValue.toHexString()
: String(changedValue)}
</div> </div>
</ColorPicker> </ColorPicker>
); );
@ -555,6 +593,8 @@ describe('ColorPicker', () => {
doMouseMove(container, 0, 999); doMouseMove(container, 0, 999);
expect(container.querySelector('.color-value')?.innerHTML).toEqual('#000000'); expect(container.querySelector('.color-value')?.innerHTML).toEqual('#000000');
expect(container.querySelector('.color-value-changed')?.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', () => { it('Should warning work when set disabledAlpha true and color is alpha color', () => {
@ -645,7 +685,7 @@ describe('ColorPicker', () => {
it('Controlled string value should work with allowClear correctly', async () => { it('Controlled string value should work with allowClear correctly', async () => {
const Demo = (props: any) => { const Demo = (props: any) => {
const [color, setColor] = useState<ColorValueType>(generateColor('red')); const [color, setColor] = useState<ColorValueType>(generateColor('#FF0000'));
useEffect(() => { useEffect(() => {
if (typeof props.value !== 'undefined') { if (typeof props.value !== 'undefined') {
@ -654,7 +694,14 @@ describe('ColorPicker', () => {
}, [props.value]); }, [props.value]);
return ( 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 />); const { container, rerender } = render(<Demo />);
@ -662,10 +709,13 @@ describe('ColorPicker', () => {
expect( expect(
container.querySelector('.ant-color-picker-trigger .ant-color-picker-clear'), container.querySelector('.ant-color-picker-trigger .ant-color-picker-clear'),
).toBeFalsy(); ).toBeFalsy();
// Clear
fireEvent.click(container.querySelector('.ant-color-picker-clear')!); fireEvent.click(container.querySelector('.ant-color-picker-clear')!);
expect( expect(
container.querySelector('.ant-color-picker-trigger .ant-color-picker-clear'), container.querySelector('.ant-color-picker-trigger .ant-color-picker-clear'),
).toBeTruthy(); ).toBeTruthy();
rerender(<Demo value="#1677ff" />); rerender(<Demo value="#1677ff" />);
expect( expect(
container.querySelector('.ant-color-picker-trigger .ant-color-picker-clear'), container.querySelector('.ant-color-picker-trigger .ant-color-picker-clear'),

View File

@ -1,23 +1,52 @@
/* eslint-disable class-methods-use-this */ /* eslint-disable class-methods-use-this */
import type { ColorGenInput } from '@rc-component/color-picker';
import { Color as RcColor } 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) => export const toHexFormat = (value?: string, alpha?: boolean) =>
value?.replace(/[^\w/]/gi, '').slice(0, alpha ? 8 : 6) || ''; value?.replace(/[^\w/]/gi, '').slice(0, alpha ? 8 : 6) || '';
export const getHex = (value?: string, alpha?: boolean) => (value ? toHexFormat(value, alpha) : ''); export const getHex = (value?: string, alpha?: boolean) => (value ? toHexFormat(value, alpha) : '');
export type GradientColor = {
color: AggregationColor;
percent: number;
}[];
export class AggregationColor { export class AggregationColor {
/** Original Color object */ /** Original Color object */
private metaColor: RcColor; private metaColor: RcColor;
public cleared: boolean | 'controlled' = false; private colors: GradientColor | undefined;
constructor(color: ColorGenInput<AggregationColor>) { public cleared = false;
this.metaColor = new RcColor(color instanceof AggregationColor ? color.metaColor : color);
if (!color) { constructor(color: ColorGenInput<AggregationColor> | Colors<AggregationColor>) {
this.metaColor.setAlpha(0); // 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; this.cleared = true;
} }
} }
@ -31,7 +60,7 @@ export class AggregationColor {
} }
toHex() { toHex() {
return getHex(this.toHexString(), this.metaColor.getAlpha() < 1); return getHex(this.toHexString(), this.metaColor.a < 1);
} }
toHexString() { toHexString() {
@ -45,4 +74,42 @@ export class AggregationColor {
toRgbString() { toRgbString() {
return this.metaColor.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

@ -2,11 +2,11 @@ import type { FC } from 'react';
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import type { AggregationColor } from '../color'; import type { AggregationColor } from '../color';
import type { ColorPickerBaseProps } from '../interface'; import { generateColor, getColorAlpha } from '../util';
import { generateColor, getAlphaColor } from '../util';
import ColorSteppers from './ColorSteppers'; import ColorSteppers from './ColorSteppers';
interface ColorAlphaInputProps extends Pick<ColorPickerBaseProps, 'prefixCls'> { interface ColorAlphaInputProps {
prefixCls: string;
value?: AggregationColor; value?: AggregationColor;
onChange?: (value: AggregationColor) => void; onChange?: (value: AggregationColor) => void;
} }
@ -34,7 +34,7 @@ const ColorAlphaInput: FC<ColorAlphaInputProps> = ({ prefixCls, value, onChange
return ( return (
<ColorSteppers <ColorSteppers
value={getAlphaColor(alphaValue)} value={getColorAlpha(alphaValue)}
prefixCls={prefixCls} prefixCls={prefixCls}
formatter={(step) => `${step}%`} formatter={(step) => `${step}%`}
className={colorAlphaInputPrefixCls} className={colorAlphaInputPrefixCls}

View File

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

View File

@ -4,10 +4,10 @@ import React, { useEffect, useState } from 'react';
import Input from '../../input'; import Input from '../../input';
import type { AggregationColor } from '../color'; import type { AggregationColor } from '../color';
import { toHexFormat } from '../color'; import { toHexFormat } from '../color';
import type { ColorPickerBaseProps } from '../interface';
import { generateColor } from '../util'; import { generateColor } from '../util';
interface ColorHexInputProps extends Pick<ColorPickerBaseProps, 'prefixCls'> { interface ColorHexInputProps {
prefixCls: string;
value?: AggregationColor; value?: AggregationColor;
onChange?: (value: AggregationColor) => void; onChange?: (value: AggregationColor) => void;
} }

View File

@ -3,11 +3,11 @@ import React, { useEffect, useState } from 'react';
import type { HSB } from '@rc-component/color-picker'; import type { HSB } from '@rc-component/color-picker';
import type { AggregationColor } from '../color'; import type { AggregationColor } from '../color';
import type { ColorPickerBaseProps } from '../interface';
import { generateColor, getRoundNumber } from '../util'; import { generateColor, getRoundNumber } from '../util';
import ColorSteppers from './ColorSteppers'; import ColorSteppers from './ColorSteppers';
interface ColorHsbInputProps extends Pick<ColorPickerBaseProps, 'prefixCls'> { interface ColorHsbInputProps {
prefixCls: string;
value?: AggregationColor; value?: AggregationColor;
onChange?: (value: AggregationColor) => void; onChange?: (value: AggregationColor) => void;
} }

View File

@ -4,15 +4,18 @@ import useMergedState from 'rc-util/lib/hooks/useMergedState';
import Select from '../../select'; import Select from '../../select';
import type { AggregationColor } from '../color'; import type { AggregationColor } from '../color';
import type { ColorFormatType, ColorPickerBaseProps } from '../interface'; import type { ColorFormatType } from '../interface';
import { ColorFormat } from '../interface'; import { ColorFormat } from '../interface';
import ColorAlphaInput from './ColorAlphaInput'; import ColorAlphaInput from './ColorAlphaInput';
import ColorHexInput from './ColorHexInput'; import ColorHexInput from './ColorHexInput';
import ColorHsbInput from './ColorHsbInput'; import ColorHsbInput from './ColorHsbInput';
import ColorRgbInput from './ColorRgbInput'; import ColorRgbInput from './ColorRgbInput';
interface ColorInputProps interface ColorInputProps {
extends Pick<ColorPickerBaseProps, 'prefixCls' | 'format' | 'onFormatChange' | 'disabledAlpha'> { prefixCls: string;
format?: ColorFormatType;
onFormatChange?: (format: ColorFormatType) => void;
disabledAlpha?: boolean;
value?: AggregationColor; value?: AggregationColor;
onChange?: (value: AggregationColor) => void; onChange?: (value: AggregationColor) => void;
} }

View File

@ -9,10 +9,11 @@ import Collapse from '../../collapse';
import { useLocale } from '../../locale'; import { useLocale } from '../../locale';
import { useToken } from '../../theme/internal'; import { useToken } from '../../theme/internal';
import type { AggregationColor } from '../color'; import type { AggregationColor } from '../color';
import type { ColorPickerBaseProps, PresetsItem } from '../interface'; import type { PresetsItem } from '../interface';
import { generateColor } from '../util'; import { generateColor } from '../util';
interface ColorPresetsProps extends Pick<ColorPickerBaseProps, 'prefixCls'> { interface ColorPresetsProps {
prefixCls: string;
presets: PresetsItem[]; presets: PresetsItem[];
value?: AggregationColor; value?: AggregationColor;
onChange?: (value: AggregationColor) => void; onChange?: (value: AggregationColor) => void;

View File

@ -3,11 +3,11 @@ import React, { useEffect, useState } from 'react';
import type { RGB } from '@rc-component/color-picker'; import type { RGB } from '@rc-component/color-picker';
import type { AggregationColor } from '../color'; import type { AggregationColor } from '../color';
import type { ColorPickerBaseProps } from '../interface';
import { generateColor } from '../util'; import { generateColor } from '../util';
import ColorSteppers from './ColorSteppers'; import ColorSteppers from './ColorSteppers';
interface ColorRgbInputProps extends Pick<ColorPickerBaseProps, 'prefixCls'> { interface ColorRgbInputProps {
prefixCls: string;
value?: AggregationColor; value?: AggregationColor;
onChange?: (value: AggregationColor) => void; onChange?: (value: AggregationColor) => void;
} }

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 type { InputNumberProps } from '../../input-number';
import InputNumber 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; value?: number;
min?: number; min?: number;
max?: number; max?: number;

View File

@ -1,16 +1,21 @@
/* eslint-disable react/no-array-index-key */
import type { CSSProperties, MouseEventHandler } from 'react'; import type { CSSProperties, MouseEventHandler } from 'react';
import React, { forwardRef, useMemo } from 'react'; import React, { forwardRef, useMemo } from 'react';
import { ColorBlock } from '@rc-component/color-picker'; import { ColorBlock } from '@rc-component/color-picker';
import classNames from 'classnames'; import classNames from 'classnames';
import pickAttrs from 'rc-util/lib/pickAttrs'; import pickAttrs from 'rc-util/lib/pickAttrs';
import type { ColorPickerBaseProps, ColorPickerProps } from '../interface'; import { useLocale } from '../../locale';
import { getAlphaColor } from '../util'; import type { AggregationColor } from '../color';
import type { ColorFormatType, ColorPickerProps } from '../interface';
import { getColorAlpha } from '../util';
import ColorClear from './ColorClear'; import ColorClear from './ColorClear';
export interface ColorTriggerProps export interface ColorTriggerProps {
extends Pick<ColorPickerBaseProps, 'prefixCls' | 'disabled' | 'format'> { prefixCls: string;
color: NonNullable<ColorPickerBaseProps['color']>; disabled?: boolean;
format?: ColorFormatType;
color: AggregationColor;
open?: boolean; open?: boolean;
showText?: ColorPickerProps['showText']; showText?: ColorPickerProps['showText'];
className?: string; className?: string;
@ -18,25 +23,57 @@ export interface ColorTriggerProps
onClick?: MouseEventHandler<HTMLDivElement>; onClick?: MouseEventHandler<HTMLDivElement>;
onMouseEnter?: MouseEventHandler<HTMLDivElement>; onMouseEnter?: MouseEventHandler<HTMLDivElement>;
onMouseLeave?: MouseEventHandler<HTMLDivElement>; onMouseLeave?: MouseEventHandler<HTMLDivElement>;
activeIndex: number;
} }
const ColorTrigger = forwardRef<HTMLDivElement, ColorTriggerProps>((props, ref) => { 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 colorTriggerPrefixCls = `${prefixCls}-trigger`;
const colorTextPrefixCls = `${colorTriggerPrefixCls}-text`;
const colorTextCellPrefixCls = `${colorTextPrefixCls}-cell`;
const containerNode = useMemo<React.ReactNode>( const [locale] = useLocale('ColorPicker');
() =>
color.cleared ? ( // ============================== Text ==============================
<ColorClear prefixCls={prefixCls} /> const desc: React.ReactNode = React.useMemo(() => {
) : ( if (!showText) {
<ColorBlock prefixCls={prefixCls} color={color.toRgbString()} /> return '';
), }
[color, prefixCls],
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 hexString = color.toHexString().toUpperCase();
const alpha = getAlphaColor(color); const alpha = getColorAlpha(color);
switch (format) { switch (format) {
case 'rgb': case 'rgb':
return color.toRgbString(); return color.toRgbString();
@ -46,16 +83,18 @@ const ColorTrigger = forwardRef<HTMLDivElement, ColorTriggerProps>((props, ref)
default: default:
return alpha < 100 ? `${hexString.slice(0, 7)},${alpha}%` : hexString; return alpha < 100 ? `${hexString.slice(0, 7)},${alpha}%` : hexString;
} }
}; }, [color, format, showText, activeIndex]);
const renderText = () => { // ============================= Render =============================
if (typeof showText === 'function') { const containerNode = useMemo<React.ReactNode>(
return showText(color); () =>
} color.cleared ? (
if (showText) { <ColorClear prefixCls={prefixCls} />
return genColorString(); ) : (
} <ColorBlock prefixCls={prefixCls} color={color.toCssString()} />
}; ),
[color, prefixCls],
);
return ( return (
<div <div
@ -67,7 +106,7 @@ const ColorTrigger = forwardRef<HTMLDivElement, ColorTriggerProps>((props, ref)
{...pickAttrs(rest)} {...pickAttrs(rest)}
> >
{containerNode} {containerNode}
{showText && <div className={`${colorTriggerPrefixCls}-text`}>{renderText()}</div>} {showText && <div className={colorTextPrefixCls}>{desc}</div>}
</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 { AggregationColor } 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?: AggregationColor;
onChange?: (value?: AggregationColor, 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,161 @@
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) => {
const nextColor = generateColor(colorValue);
onChange(fillColor(value.cleared ? genAlphaColor(nextColor) : nextColor), 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) => {
onInternalChange(colorValue, true);
}}
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 type { FC } from 'react';
import React, { useContext } from 'react'; import React, { useContext } from 'react';
import type { AggregationColor } from '../color';
import { PanelPresetsContext } from '../context'; import { PanelPresetsContext } from '../context';
import type { ColorPickerBaseProps } from '../interface';
import ColorPresets from './ColorPresets'; import ColorPresets from './ColorPresets';
export interface PanelPresetsProps extends Pick<ColorPickerBaseProps, 'prefixCls' | 'presets'> {
value?: AggregationColor;
onChange?: (value: AggregationColor) => void;
}
const PanelPresets: FC = () => { const PanelPresets: FC = () => {
const { prefixCls, value, presets, onChange } = useContext(PanelPresetsContext); const { prefixCls, value, presets, onChange } = useContext(PanelPresetsContext);
return Array.isArray(presets) ? ( return Array.isArray(presets) ? (

View File

@ -1,11 +1,50 @@
import React from 'react'; import React from 'react';
import type { PanelPickerProps } from './components/PanelPicker'; import type { GetProp } from '../_util/type';
import type { PanelPresetsProps } from './components/PanelPresets'; 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; format?: ColorFormatType;
export const { Provider: PanelPresetsProvider } = PanelPresetsContext; 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 { ColorPicker, Space } from 'antd';
import type { ColorPickerProps, GetProp } 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'>; type Format = GetProp<ColorPickerProps, 'format'>;
const HexCase: React.FC = () => { const HexCase: React.FC = () => {
@ -28,7 +28,7 @@ const HexCase: React.FC = () => {
}; };
const HsbCase: 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 [formatHsb, setFormatHsb] = useState<ColorPickerProps['format']>('hsb');
const hsbString = React.useMemo( const hsbString = React.useMemo(
@ -50,7 +50,7 @@ const HsbCase: React.FC = () => {
}; };
const RgbCase: 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 [formatRgb, setFormatRgb] = useState<ColorPickerProps['format']>('rgb');
const rgbString = React.useMemo( 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); const [open, setOpen] = useState(false);
return ( return (
<Space direction="vertical"> <Space direction="vertical">
<ColorPicker defaultValue="#1677ff" showText /> <ColorPicker defaultValue="#1677ff" showText allowClear />
<ColorPicker <ColorPicker
defaultValue="#1677ff" defaultValue="#1677ff"
showText={(color) => <span>Custom Text ({color.toHexString()})</span>} 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 { Button, ColorPicker } from 'antd';
import type { ColorPickerProps, GetProp } 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 Demo: React.FC = () => {
const [color, setColor] = useState<Color>('#1677ff'); const [color, setColor] = useState<Color>('#1677ff');

View File

@ -1,56 +0,0 @@
import { useEffect, useRef, useState } from 'react';
import type { AggregationColor } 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<AggregationColor>(generateColor(''));
const [colorValue, _setColorValue] = useState<AggregationColor>(() => {
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: AggregationColor) => {
_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/size.tsx">Trigger size</code>
<code src="./demo/controlled.tsx">controlled mode</code> <code src="./demo/controlled.tsx">controlled mode</code>
<code src="./demo/change-completed.tsx">Color change completed</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/text-render.tsx">Rendering Trigger Text</code>
<code src="./demo/disabled.tsx">Disable</code> <code src="./demo/disabled.tsx">Disable</code>
<code src="./demo/disabled-alpha.tsx">Disabled Alpha</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 | | disabledAlpha | Disable Alpha | boolean | - | 5.8.0 |
| destroyTooltipOnHide | Whether destroy popover when hidden | `boolean` | false | 5.7.0 | | destroyTooltipOnHide | Whether destroy popover when hidden | `boolean` | false | 5.7.0 |
| format | Format of color | `rgb` \| `hex` \| `hsb` | `hex` | | | 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 | - | | | open | Whether to show popup | boolean | - | |
| presets | Preset colors | `{ label: ReactNode, colors: Array<string \| Color>, defaultOpen?: boolean }[]` | - | `defaultOpen: 5.11.0` | | 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` | | | 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 ### Color
<!-- prettier-ignore --> <!-- 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` | - | | 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` | - | | 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 })` | - | | toHsb | Convert to `hsb` object | `() => ({ h: number, s: number, b: number, a number })` | - |

View File

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

View File

@ -1,11 +1,21 @@
import type { CSSProperties, FC, ReactNode } from 'react'; 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 { SizeType } from '../config-provider/SizeContext';
import type { PopoverProps } from '../popover'; import type { PopoverProps } from '../popover';
import type { TooltipPlacement } from '../tooltip'; import type { TooltipPlacement } from '../tooltip';
import type { AggregationColor } from './color'; import type { AggregationColor } from './color';
export type { ColorGenInput };
export type Colors<T> = {
color: ColorGenInput<T>;
percent: number;
}[];
export enum ColorFormat { export enum ColorFormat {
hex = 'hex', hex = 'hex',
rgb = 'rgb', rgb = 'rgb',
@ -28,25 +38,29 @@ export type TriggerType = 'click' | 'hover';
export type TriggerPlacement = TooltipPlacement; // Alias, to prevent breaking changes. export type TriggerPlacement = TooltipPlacement; // Alias, to prevent breaking changes.
export interface ColorPickerBaseProps { export type SingleValueType = AggregationColor | string;
color?: AggregationColor;
prefixCls: string;
format?: ColorFormatType;
allowClear?: boolean;
disabled?: boolean;
disabledAlpha?: boolean;
presets?: PresetsItem[];
panelRender?: ColorPickerProps['panelRender'];
onFormatChange?: ColorPickerProps['onFormatChange'];
onChangeComplete?: ColorPickerProps['onChangeComplete'];
}
export type ColorValueType = AggregationColor | string | null; export type ColorValueType =
| SingleValueType
| null
| {
color: SingleValueType;
percent: number;
}[];
export type ModeType = 'single' | 'gradient';
export type ColorPickerProps = Omit< export type ColorPickerProps = Omit<
RcColorPickerProps, RcColorPickerProps,
'onChange' | 'value' | 'defaultValue' | 'panelRender' | 'disabledAlpha' | 'onChangeComplete' | 'onChange'
| 'value'
| 'defaultValue'
| 'panelRender'
| 'disabledAlpha'
| 'onChangeComplete'
| 'components'
> & { > & {
mode?: ModeType | ModeType[];
value?: ColorValueType; value?: ColorValueType;
defaultValue?: ColorValueType; defaultValue?: ColorValueType;
children?: React.ReactNode; children?: React.ReactNode;

View File

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

View File

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

View File

@ -2,7 +2,6 @@ import { unit } from '@ant-design/cssinjs';
import type { CSSObject } from '@ant-design/cssinjs'; import type { CSSObject } from '@ant-design/cssinjs';
import type { GenerateStyle } from '../../theme/internal'; import type { GenerateStyle } from '../../theme/internal';
import { getTransBg } from './color-block';
import type { ColorPickerToken } from './index'; import type { ColorPickerToken } from './index';
const genPickerStyle: GenerateStyle<ColorPickerToken, CSSObject> = (token) => { const genPickerStyle: GenerateStyle<ColorPickerToken, CSSObject> = (token) => {
@ -16,11 +15,11 @@ const genPickerStyle: GenerateStyle<ColorPickerToken, CSSObject> = (token) => {
colorFillSecondary, colorFillSecondary,
lineWidthBold, lineWidthBold,
colorPickerHandlerSize, colorPickerHandlerSize,
colorPickerHandlerSizeSM,
colorPickerSliderHeight,
} = token; } = token;
return { return {
userSelect: 'none',
[`${componentCls}-select`]: { [`${componentCls}-select`]: {
[`${componentCls}-palette`]: { [`${componentCls}-palette`]: {
minHeight: token.calc(controlHeightLG).mul(4).equal(), minHeight: token.calc(controlHeightLG).mul(4).equal(),
@ -36,6 +35,7 @@ const genPickerStyle: GenerateStyle<ColorPickerToken, CSSObject> = (token) => {
marginBottom: marginSM, marginBottom: marginSM,
}, },
// ======================== Panel =========================
[`${componentCls}-handler`]: { [`${componentCls}-handler`]: {
width: colorPickerHandlerSize, width: colorPickerHandlerSize,
height: colorPickerHandlerSize, height: colorPickerHandlerSize,
@ -44,40 +44,6 @@ const genPickerStyle: GenerateStyle<ColorPickerToken, CSSObject> = (token) => {
borderRadius: '50%', borderRadius: '50%',
cursor: 'pointer', cursor: 'pointer',
boxShadow: `${colorPickerInsetShadow}, 0 0 0 1px ${colorFillSecondary}`, 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,8 +1,12 @@
import type { ColorGenInput } from '@rc-component/color-picker'; import type { ColorGenInput } from '@rc-component/color-picker';
import { Color as RcColor } from '@rc-component/color-picker';
import { AggregationColor } from './color'; import { AggregationColor } from './color';
import type { ColorValueType } from './interface';
export const generateColor = (color: ColorGenInput<AggregationColor>): AggregationColor => { export const generateColor = (
color: ColorGenInput<AggregationColor> | Exclude<ColorValueType, null>,
): AggregationColor => {
if (color instanceof AggregationColor) { if (color instanceof AggregationColor) {
return color; return color;
} }
@ -11,10 +15,55 @@ export const generateColor = (color: ColorGenInput<AggregationColor>): Aggregati
export const getRoundNumber = (value: number) => Math.round(Number(value || 0)); export const getRoundNumber = (value: number) => Math.round(Number(value || 0));
export const getAlphaColor = (color: AggregationColor) => getRoundNumber(color.toHsb().a * 100); export const getColorAlpha = (color: AggregationColor) => getRoundNumber(color.toHsb().a * 100);
/** Return the color whose `alpha` is 1 */
export const genAlphaColor = (color: AggregationColor, alpha?: number) => { export const genAlphaColor = (color: AggregationColor, alpha?: number) => {
const hsba = color.toHsb(); const hsba = color.toHsb();
hsba.a = alpha || 1; hsba.a = alpha || 1;
return generateColor(hsba); 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

@ -12,7 +12,7 @@ import {
} from 'antd'; } from 'antd';
import type { ColorPickerProps, GetProp } 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 = { type ThemeData = {
borderRadius: number; borderRadius: number;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -457,11 +457,7 @@ export default () => {
<Anchor> <Anchor>
<Link href="#anchor-demo-basic" title="Basic demo" /> <Link href="#anchor-demo-basic" title="Basic demo" />
<Link href="#anchor-demo-static" title="Static demo" /> <Link href="#anchor-demo-static" title="Static demo" />
<Link <Link href="#anchor-demo-basic" title="Basic demo with Target" target="_blank" />
href="#anchor-demo-basic"
title="Basic demo with Target"
target="_blank"
/>
<Link href="#API" title="API"> <Link href="#API" title="API">
<Link href="#Anchor-Props" title="Anchor Props" /> <Link href="#Anchor-Props" title="Anchor Props" />
<Link href="#Link-Props" title="Link Props" /> <Link href="#Link-Props" title="Link Props" />

View File

@ -3,12 +3,7 @@ import { Keyframes, unit } from '@ant-design/cssinjs';
import { CONTAINER_MAX_OFFSET } from '../../_util/hooks/useZIndex'; import { CONTAINER_MAX_OFFSET } from '../../_util/hooks/useZIndex';
import { genFocusStyle, resetComponent } from '../../style'; import { genFocusStyle, resetComponent } from '../../style';
import type { import type { AliasToken, FullToken, GenerateStyle, GenStyleFn } from '../../theme/internal';
AliasToken,
FullToken,
GenerateStyle,
GenStyleFn,
} from '../../theme/internal';
import { genStyleHooks, mergeToken } from '../../theme/internal'; import { genStyleHooks, mergeToken } from '../../theme/internal';
import genNotificationPlacementStyle from './placement'; import genNotificationPlacementStyle from './placement';
import genStackStyle from './stack'; import genStackStyle from './stack';

View File

@ -10,12 +10,7 @@ import {
import type { SharedComponentToken, SharedInputToken } from '../../input/style/token'; import type { SharedComponentToken, SharedInputToken } from '../../input/style/token';
import { genBaseOutlinedStyle, genDisabledStyle } from '../../input/style/variants'; import { genBaseOutlinedStyle, genDisabledStyle } from '../../input/style/variants';
import { genFocusOutline, genFocusStyle, resetComponent } from '../../style'; import { genFocusOutline, genFocusStyle, resetComponent } from '../../style';
import type { import type { FullToken, GenerateStyle, GetDefaultToken, GenStyleFn } from '../../theme/internal';
FullToken,
GenerateStyle,
GetDefaultToken,
GenStyleFn,
} from '../../theme/internal';
import { genStyleHooks, mergeToken } from '../../theme/internal'; import { genStyleHooks, mergeToken } from '../../theme/internal';
export interface ComponentToken { export interface ComponentToken {

View File

@ -1,12 +1,7 @@
import type { CSSObject } from '@ant-design/cssinjs'; import type { CSSObject } from '@ant-design/cssinjs';
import { Keyframes, unit } from '@ant-design/cssinjs'; import { Keyframes, unit } from '@ant-design/cssinjs';
import type { import type { FullToken, GenerateStyle, GetDefaultToken, CSSUtil } from '../../theme/internal';
FullToken,
GenerateStyle,
GetDefaultToken,
CSSUtil,
} from '../../theme/internal';
import { genStyleHooks, mergeToken } from '../../theme/internal'; import { genStyleHooks, mergeToken } from '../../theme/internal';
export type ComponentToken = { export type ComponentToken = {

View File

@ -0,0 +1,14 @@
import { createContext } from 'react';
import type { SliderProps as RcSliderProps } from 'rc-slider';
import type { DirectionType } from '../config-provider';
export interface SliderInternalContextProps {
handleRender?: RcSliderProps['handleRender'];
direction?: DirectionType;
}
/** @private Internal context. Do not use in your production. */
const SliderInternalContext = createContext<SliderInternalContextProps>({});
export default SliderInternalContext;

View File

@ -12,6 +12,7 @@ import DisabledContext from '../config-provider/DisabledContext';
import type { AbstractTooltipProps, TooltipPlacement } from '../tooltip'; import type { AbstractTooltipProps, TooltipPlacement } from '../tooltip';
import SliderTooltip from './SliderTooltip'; import SliderTooltip from './SliderTooltip';
import useStyle from './style'; import useStyle from './style';
import SliderInternalContext from './Context';
import useRafLock from './useRafLock'; import useRafLock from './useRafLock';
export type SliderMarks = RcSliderProps['marks']; export type SliderMarks = RcSliderProps['marks'];
@ -146,10 +147,22 @@ const Slider = React.forwardRef<SliderRef, SliderSingleProps | SliderRangeProps>
const { vertical } = props; const { vertical } = props;
const { direction, slider, getPrefixCls, getPopupContainer } = React.useContext(ConfigContext); const {
direction: contextDirection,
slider,
getPrefixCls,
getPopupContainer,
} = React.useContext(ConfigContext);
const contextDisabled = React.useContext(DisabledContext); const contextDisabled = React.useContext(DisabledContext);
const mergedDisabled = disabled ?? contextDisabled; const mergedDisabled = disabled ?? contextDisabled;
// ============================= Context ==============================
const { handleRender: contextHandleRender, direction: internalContextDirection } =
React.useContext(SliderInternalContext);
const mergedDirection = internalContextDirection || contextDirection;
const isRTL = mergedDirection === 'rtl';
// =============================== Open =============================== // =============================== Open ===============================
const [hoverOpen, setHoverOpen] = useRafLock(); const [hoverOpen, setHoverOpen] = useRafLock();
const [focusOpen, setFocusOpen] = useRafLock(); const [focusOpen, setFocusOpen] = useRafLock();
@ -186,7 +199,7 @@ const Slider = React.forwardRef<SliderRef, SliderSingleProps | SliderRangeProps>
if (!vert) { if (!vert) {
return 'top'; return 'top';
} }
return direction === 'rtl' ? 'left' : 'right'; return isRTL ? 'left' : 'right';
}; };
// ============================== Style =============================== // ============================== Style ===============================
@ -199,7 +212,7 @@ const Slider = React.forwardRef<SliderRef, SliderSingleProps | SliderRangeProps>
slider?.className, slider?.className,
rootClassName, rootClassName,
{ {
[`${prefixCls}-rtl`]: direction === 'rtl', [`${prefixCls}-rtl`]: isRTL,
[`${prefixCls}-lock`]: dragging, [`${prefixCls}-lock`]: dragging,
}, },
hashId, hashId,
@ -207,7 +220,7 @@ const Slider = React.forwardRef<SliderRef, SliderSingleProps | SliderRangeProps>
); );
// make reverse default on rtl direction // make reverse default on rtl direction
if (direction === 'rtl' && !restProps.vertical) { if (isRTL && !restProps.vertical) {
restProps.reverse = !restProps.reverse; restProps.reverse = !restProps.reverse;
} }
@ -246,35 +259,49 @@ const Slider = React.forwardRef<SliderRef, SliderSingleProps | SliderRangeProps>
const useActiveTooltipHandle = range && !lockOpen; const useActiveTooltipHandle = range && !lockOpen;
const handleRender: RcSliderProps['handleRender'] = (node, info) => { const handleRender: RcSliderProps['handleRender'] =
contextHandleRender ||
((node, info) => {
const { index } = info; const { index } = info;
const nodeProps = node.props; const nodeProps = node.props;
function proxyEvent(
eventName: string,
event: React.SyntheticEvent,
triggerRestPropsEvent?: boolean,
) {
if (triggerRestPropsEvent) {
(restProps as any)[eventName]?.(event);
}
(nodeProps as any)[eventName]?.(event);
}
const passedProps: typeof nodeProps = { const passedProps: typeof nodeProps = {
...nodeProps, ...nodeProps,
onMouseEnter: (e) => { onMouseEnter: (e) => {
setHoverOpen(true); setHoverOpen(true);
nodeProps.onMouseEnter?.(e); proxyEvent('onMouseEnter', e);
}, },
onMouseLeave: (e) => { onMouseLeave: (e) => {
setHoverOpen(false); setHoverOpen(false);
nodeProps.onMouseLeave?.(e); proxyEvent('onMouseLeave', e);
}, },
onMouseDown: (e) => { onMouseDown: (e) => {
setFocusOpen(true); setFocusOpen(true);
setDragging(true); setDragging(true);
nodeProps.onMouseDown?.(e); proxyEvent('onMouseDown', e);
}, },
onFocus: (e) => { onFocus: (e) => {
setFocusOpen(true); setFocusOpen(true);
restProps.onFocus?.(e); restProps.onFocus?.(e);
nodeProps.onFocus?.(e); proxyEvent('onFocus', e, true);
}, },
onBlur: (e) => { onBlur: (e) => {
setFocusOpen(false); setFocusOpen(false);
restProps.onBlur?.(e); restProps.onBlur?.(e);
nodeProps.onBlur?.(e); proxyEvent('onBlur', e, true);
}, },
}; };
@ -303,7 +330,7 @@ const Slider = React.forwardRef<SliderRef, SliderSingleProps | SliderRangeProps>
} }
return cloneNode; return cloneNode;
}; });
// ========================== Active Handle =========================== // ========================== Active Handle ===========================
const activeHandleRender: SliderProps['activeHandleRender'] = useActiveTooltipHandle const activeHandleRender: SliderProps['activeHandleRender'] = useActiveTooltipHandle

View File

@ -198,6 +198,7 @@ const genBaseStyle: GenerateStyle<SliderToken> = (token) => {
width: handleSize, width: handleSize,
height: handleSize, height: handleSize,
outline: 'none', outline: 'none',
userSelect: 'none',
// Dragging status // Dragging status
'&-dragging-delete': { '&-dragging-delete': {

View File

@ -1,12 +1,7 @@
/* eslint-disable import/prefer-default-export */ /* eslint-disable import/prefer-default-export */
import type { CSSInterpolation, CSSObject } from '@ant-design/cssinjs'; import type { CSSInterpolation, CSSObject } from '@ant-design/cssinjs';
import type { import type { AliasToken, FullToken, OverrideComponent, CSSUtil } from '../theme/internal';
AliasToken,
FullToken,
OverrideComponent,
CSSUtil,
} from '../theme/internal';
function compactItemVerticalBorder(token: AliasToken & CSSUtil, parentCls: string): CSSObject { function compactItemVerticalBorder(token: AliasToken & CSSUtil, parentCls: string): CSSObject {
return { return {

View File

@ -1,12 +1,7 @@
/* eslint-disable import/prefer-default-export */ /* eslint-disable import/prefer-default-export */
import type { CSSInterpolation, CSSObject } from '@ant-design/cssinjs'; import type { CSSInterpolation, CSSObject } from '@ant-design/cssinjs';
import type { import type { AliasToken, FullToken, OverrideComponent, CSSUtil } from '../theme/internal';
AliasToken,
FullToken,
OverrideComponent,
CSSUtil,
} from '../theme/internal';
interface CompactItemOptions { interface CompactItemOptions {
focus?: boolean; focus?: boolean;

View File

@ -12,10 +12,7 @@ export const textEllipsis: CSSObject = {
textOverflow: 'ellipsis', textOverflow: 'ellipsis',
}; };
export const resetComponent = ( export const resetComponent = (token: AliasToken, needInheritFontFamily = false): CSSObject => ({
token: AliasToken,
needInheritFontFamily = false,
): CSSObject => ({
boxSizing: 'border-box', boxSizing: 'border-box',
margin: 0, margin: 0,
padding: 0, padding: 0,

View File

@ -176,11 +176,7 @@ const App: React.FC = () => {
return ( return (
<> <>
<Form <Form layout="inline" className="table-demo-control-bar" style={{ marginBottom: 16 }}>
layout="inline"
className="table-demo-control-bar"
style={{ marginBottom: 16 }}
>
<Form.Item label="Bordered"> <Form.Item label="Bordered">
<Switch checked={bordered} onChange={handleBorderChange} /> <Switch checked={bordered} onChange={handleBorderChange} />
</Form.Item> </Form.Item>

View File

@ -176,11 +176,7 @@ const App: React.FC = () => {
return ( return (
<> <>
<Form <Form layout="inline" className="table-demo-control-bar" style={{ marginBottom: 16 }}>
layout="inline"
className="table-demo-control-bar"
style={{ marginBottom: 16 }}
>
<Form.Item label="Bordered"> <Form.Item label="Bordered">
<Switch checked={bordered} onChange={handleBorderChange} /> <Switch checked={bordered} onChange={handleBorderChange} />
</Form.Item> </Form.Item>

View File

@ -99,11 +99,7 @@ const App: React.FC = () => {
const [childTableBordered, setChildTableBordered] = useState(true); const [childTableBordered, setChildTableBordered] = useState(true);
return ( return (
<> <>
<Form <Form layout="inline" className="table-demo-control-bar" style={{ marginBottom: 16 }}>
layout="inline"
className="table-demo-control-bar"
style={{ marginBottom: 16 }}
>
<Form.Item label="Root Table Bordered"> <Form.Item label="Root Table Bordered">
<Switch checked={rootTableBordered} onChange={(v) => setRootTableBordered(v)} /> <Switch checked={rootTableBordered} onChange={(v) => setRootTableBordered(v)} />
</Form.Item> </Form.Item>

View File

@ -4,11 +4,7 @@ import type { CSSInterpolation } from '@ant-design/cssinjs';
import { TinyColor } from '@ctrl/tinycolor'; import { TinyColor } from '@ctrl/tinycolor';
import { resetComponent } from '../../style'; import { resetComponent } from '../../style';
import type { import type { FullToken, GetDefaultToken, GenStyleFn } from '../../theme/internal';
FullToken,
GetDefaultToken,
GenStyleFn,
} from '../../theme/internal';
import { genStyleHooks, mergeToken } from '../../theme/internal'; import { genStyleHooks, mergeToken } from '../../theme/internal';
export interface ComponentToken { export interface ComponentToken {

View File

@ -10,7 +10,6 @@ import type {
import type { AliasToken } from './alias'; import type { AliasToken } from './alias';
import type { ComponentTokenMap } from './components'; import type { ComponentTokenMap } from './components';
/** Final token which contains the components level override */ /** Final token which contains the components level override */
export type GlobalToken = GlobalTokenTypeUtil<ComponentTokenMap, AliasToken>; export type GlobalToken = GlobalTokenTypeUtil<ComponentTokenMap, AliasToken>;
@ -18,8 +17,20 @@ export type OverrideToken = OverrideTokenTypeUtil<ComponentTokenMap, AliasToken>
export type OverrideComponent = TokenMapKey<ComponentTokenMap>; export type OverrideComponent = TokenMapKey<ComponentTokenMap>;
export type FullToken<C extends TokenMapKey<ComponentTokenMap>> = FullTokenTypeUtil<ComponentTokenMap, AliasToken, C>; export type FullToken<C extends TokenMapKey<ComponentTokenMap>> = FullTokenTypeUtil<
ComponentTokenMap,
AliasToken,
C
>;
export type GetDefaultToken<C extends TokenMapKey<ComponentTokenMap>> = GetDefaultTokenTypeUtil<ComponentTokenMap, AliasToken, C>; export type GetDefaultToken<C extends TokenMapKey<ComponentTokenMap>> = GetDefaultTokenTypeUtil<
ComponentTokenMap,
AliasToken,
C
>;
export type GenStyleFn<C extends TokenMapKey<ComponentTokenMap>> = GenStyleFnTypeUtil<ComponentTokenMap, AliasToken, C>; export type GenStyleFn<C extends TokenMapKey<ComponentTokenMap>> = GenStyleFnTypeUtil<
ComponentTokenMap,
AliasToken,
C
>;

View File

@ -1,10 +1,5 @@
import { useStyleRegister } from '@ant-design/cssinjs'; import { useStyleRegister } from '@ant-design/cssinjs';
import { import { genCalc as calc, mergeToken, statisticToken, statistic } from '@ant-design/cssinjs-utils';
genCalc as calc,
mergeToken,
statisticToken,
statistic,
} from '@ant-design/cssinjs-utils';
import type { import type {
AliasToken, AliasToken,
@ -22,11 +17,7 @@ import type {
import { PresetColors } from './interface'; import { PresetColors } from './interface';
import { getLineHeight } from './themes/shared/genFontSizes'; import { getLineHeight } from './themes/shared/genFontSizes';
import useToken from './useToken'; import useToken from './useToken';
import { import { genComponentStyleHook, genStyleHooks, genSubStyleComponent } from './util/genStyleUtils';
genComponentStyleHook,
genStyleHooks,
genSubStyleComponent,
} from './util/genStyleUtils';
import genPresetColor from './util/genPresetColor'; import genPresetColor from './util/genPresetColor';
import useResetIconStyle from './util/useResetIconStyle'; import useResetIconStyle from './util/useResetIconStyle';

View File

@ -4,12 +4,7 @@ import { Keyframes, unit } from '@ant-design/cssinjs';
import { getStyle as getCheckboxStyle } from '../../checkbox/style'; import { getStyle as getCheckboxStyle } from '../../checkbox/style';
import { genFocusOutline, resetComponent } from '../../style'; import { genFocusOutline, resetComponent } from '../../style';
import { genCollapseMotion } from '../../style/motion'; import { genCollapseMotion } from '../../style/motion';
import type { import type { AliasToken, FullToken, GetDefaultToken, CSSUtil } from '../../theme/internal';
AliasToken,
FullToken,
GetDefaultToken,
CSSUtil,
} from '../../theme/internal';
import { genStyleHooks, mergeToken } from '../../theme/internal'; import { genStyleHooks, mergeToken } from '../../theme/internal';
export interface TreeSharedToken { export interface TreeSharedToken {

View File

@ -176,11 +176,12 @@ const ListItem = React.forwardRef<HTMLDivElement, ListItemProps>(
); );
const extraContent = typeof customExtra === 'function' ? customExtra(file) : customExtra; const extraContent = typeof customExtra === 'function' ? customExtra(file) : customExtra;
const extra = extraContent && <span className={`${prefixCls}-list-item-extra`}>{extraContent}</span> const extra = extraContent && (
<span className={`${prefixCls}-list-item-extra`}>{extraContent}</span>
);
const listItemNameClass = classNames(`${prefixCls}-list-item-name`); const listItemNameClass = classNames(`${prefixCls}-list-item-name`);
const fileName = file.url const fileName = file.url ? (
?
<a <a
key="view" key="view"
target="_blank" target="_blank"
@ -194,7 +195,7 @@ const ListItem = React.forwardRef<HTMLDivElement, ListItemProps>(
{file.name} {file.name}
{extra} {extra}
</a> </a>
: ) : (
<span <span
key="view" key="view"
className={listItemNameClass} className={listItemNameClass}
@ -204,7 +205,7 @@ const ListItem = React.forwardRef<HTMLDivElement, ListItemProps>(
{file.name} {file.name}
{extra} {extra}
</span> </span>
; );
const previewIcon = const previewIcon =
showPreviewIcon && (file.url || file.thumbUrl) ? ( showPreviewIcon && (file.url || file.thumbUrl) ? (

View File

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

View File

@ -2,7 +2,7 @@ import React, { useState } from 'react';
import { ColorPicker, Flex, Form, Input, InputNumber, Slider, Typography, Watermark } from 'antd'; import { ColorPicker, Flex, Form, Input, InputNumber, Slider, Typography, Watermark } from 'antd';
import type { ColorPickerProps, GetProp, WatermarkProps } from 'antd'; import type { ColorPickerProps, GetProp, WatermarkProps } from 'antd';
type Color = GetProp<ColorPickerProps, 'color'>; type Color = Extract<GetProp<ColorPickerProps, 'value'>, string | { cleared: any }>;
const { Paragraph } = Typography; const { Paragraph } = Typography;

View File

@ -5,5 +5,8 @@
}, },
"codeSplitting": { "codeSplitting": {
"strategy": "auto" "strategy": "auto"
},
"watch": {
"_nodeModulesRegexes": ["rc-.*"]
} }
} }

View File

@ -106,7 +106,7 @@
"@ant-design/react-slick": "~1.1.2", "@ant-design/react-slick": "~1.1.2",
"@babel/runtime": "^7.24.8", "@babel/runtime": "^7.24.8",
"@ctrl/tinycolor": "^3.6.1", "@ctrl/tinycolor": "^3.6.1",
"@rc-component/color-picker": "~1.6.1", "@rc-component/color-picker": "~1.9.0",
"@rc-component/mutate-observer": "^1.1.0", "@rc-component/mutate-observer": "^1.1.0",
"@rc-component/qrcode": "~1.0.0", "@rc-component/qrcode": "~1.0.0",
"@rc-component/tour": "~1.15.0", "@rc-component/tour": "~1.15.0",
@ -135,7 +135,7 @@
"rc-resize-observer": "^1.4.0", "rc-resize-observer": "^1.4.0",
"rc-segmented": "~2.3.0", "rc-segmented": "~2.3.0",
"rc-select": "~14.15.1", "rc-select": "~14.15.1",
"rc-slider": "~11.1.0", "rc-slider": "~11.1.3",
"rc-steps": "~6.0.1", "rc-steps": "~6.0.1",
"rc-switch": "~4.1.0", "rc-switch": "~4.1.0",
"rc-table": "~7.45.7", "rc-table": "~7.45.7",

View File

@ -138,7 +138,12 @@ async function boot() {
try { try {
await retry(doUpload, 3, 1000); await retry(doUpload, 3, 1000);
} catch (err) { } catch (err) {
console.error('Uploading file `%s` failed after retry %s, error: %s', fileOrFolderName, 3, err); console.error(
'Uploading file `%s` failed after retry %s, error: %s',
fileOrFolderName,
3,
err,
);
process.exit(1); process.exit(1);
} }
return; return;
@ -152,7 +157,13 @@ async function boot() {
// eslint-disable-next-line no-await-in-loop // eslint-disable-next-line no-await-in-loop
await retry(doUpload, 3, 1000); await retry(doUpload, 3, 1000);
} catch (err) { } catch (err) {
console.warn('Skip uploading file `%s` in folder `%s` failed after retry %s, error: %s', path.relative(workspacePath, file), fileOrFolderName, 3, err); console.warn(
'Skip uploading file `%s` in folder `%s` failed after retry %s, error: %s',
path.relative(workspacePath, file),
fileOrFolderName,
3,
err,
);
} }
} }
} }