Skip to content

Commit

Permalink
New color-palette
Browse files Browse the repository at this point in the history
  • Loading branch information
DaryaLari committed Jan 23, 2025
1 parent f068744 commit 291ebfe
Show file tree
Hide file tree
Showing 11 changed files with 685 additions and 413 deletions.
23 changes: 0 additions & 23 deletions src/ui/components/ColorPicker/ColorPicker.scss

This file was deleted.

92 changes: 0 additions & 92 deletions src/ui/components/ColorPicker/ColorPicker.tsx

This file was deleted.

43 changes: 43 additions & 0 deletions src/ui/components/ColorPickerInput/ColorPickerInput.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
.color-picker-input {
&__preview {
overflow: hidden;
position: relative;
width: 16px;
height: 16px;
border-radius: 2px;
margin: 0 var(--g-spacing-1);

&::before {
content: '';
position: absolute;
inset: 0px;
border: 1px solid var(--g-color-line-generic);
}
}

&__palette {
cursor: pointer;
opacity: 0;
width: calc(100% + 8px);
height: calc(100% + 10px);
position: absolute;
left: -8px;
top: 0;
}

&__palette_size-l {
width: calc(100% + 8px);
height: calc(100% + 12px);
}

&__opacity-input {
width: 54px;
margin-inline-end: -1px;
border-inline-start: 1px solid var(--g-color-line-generic);
padding-inline-start: var(--g-spacing-1);
}

&__opacity-end-content {
margin-inline: var(--g-spacing-1);
}
}
175 changes: 175 additions & 0 deletions src/ui/components/ColorPickerInput/ColorPickerInput.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
import React from 'react';

import type {InputControlSize} from '@gravity-ui/uikit';
import {Text, TextInput} from '@gravity-ui/uikit';
import {
unstable_NumberInput as NumberInput,
type unstable_NumberInputProps as NumberInputProps,
} from '@gravity-ui/uikit/unstable';
import block from 'bem-cn-lite';
import chroma from 'chroma-js';

import {
colorMask,
getColorParts,
getMaskedColor,
getResultColorFromParts,
isEmptyColor,
isValidColor,
normalizeColor,
sanitizeColor,
} from './utils';

import './ColorPickerInput.scss';

const b = block('color-picker-input');

const DEFAULT_COLOR = '#ffffff';

interface ColorPickerInputProps {
required?: boolean;
placeholder?: string;
value?: string;
size?: InputControlSize;
hasClear?: boolean;
hasOpacityInput?: boolean;
autoFocus?: boolean;
onUpdate: (value: string | null) => void;
onValidChange?: (isValid: boolean) => void;
className?: string;
}

export interface ColorParts {
solid: string;
opacity: number | null;
}

export function ColorPickerInput({
required,
size,
placeholder,
value,
hasClear,
hasOpacityInput,
autoFocus,
onUpdate,
onValidChange,
className,
}: ColorPickerInputProps) {
const {solid: externalSolidColorPart} = getColorParts(value);
const [stateValue, setStateValue] = React.useState<ColorParts>(getColorParts(colorMask(value)));
const [isValid, setIsValid] = React.useState(true);
const pickerRef = React.useRef(null);

React.useEffect(() => {
setStateValue((prevValue) => {
if (
getResultColorFromParts(prevValue) === getResultColorFromParts(getColorParts(value))
) {
return prevValue;
}

return getMaskedColor(getColorParts(value));
});
}, [value]);

const setColor = React.useCallback(
(color: string) => {
setStateValue((prevValue) => {
let isValidValue = false;
const maskedColor = colorMask(color);
const opacity = prevValue.opacity === null ? 100 : prevValue.opacity;

if (isEmptyColor(maskedColor)) {
onUpdate(null);
isValidValue = !required;
} else if (isValidColor(maskedColor)) {
const valueWithOpacity = chroma(maskedColor)
.alpha(opacity / 100)
.hex();
onUpdate(valueWithOpacity);
isValidValue = true;
}
setIsValid(isValidValue);
onValidChange?.(isValidValue);

return {...prevValue, solid: maskedColor, opacity};
});
},
[onUpdate, onValidChange, required],
);

const handleOpacityChange = React.useCallback(
(newOpacity: number | null) => {
setStateValue((prevValue) => {
const newState = {...prevValue, opacity: newOpacity};
if (isValidColor(prevValue.solid)) {
onUpdate(getResultColorFromParts(newState));
}
return newState;
});
},
[onUpdate],
);

return (
<TextInput
className={b(null, className)}
size={size}
value={normalizeColor(stateValue.solid)}
placeholder={placeholder ? sanitizeColor(placeholder) : '#'}
onUpdate={setColor}
error={!isValid}
hasClear={hasClear}
autoFocus={autoFocus}
startContent={
<div
className={b('preview')}
style={
isValid
? {
background: `linear-gradient(90deg, ${externalSolidColorPart ?? 'transparent'} 50%, ${value ?? 'transparent'} 50%)`,
}
: {}
}
ref={pickerRef}
>
<input
className={b('palette', {[`size-${size}`]: true})}
type="color"
value={sanitizeColor(value || placeholder || DEFAULT_COLOR)}
onChange={(e) => {
setColor(e.target.value);
}}
/>
</div>
}
unstable_endContent={
hasOpacityInput ? (
<OpacityInput value={stateValue.opacity} onUpdate={handleOpacityChange} />
) : null
}
/>
);
}

function OpacityInput({value, onUpdate}: Pick<NumberInputProps, 'value' | 'onUpdate'>) {
return (
<NumberInput
className={b('opacity-input')}
view="clear"
pin="brick-round"
hiddenControls
min={0}
max={100}
placeholder="100"
endContent={
<Text className={b('opacity-end-content')} color="hint">
%
</Text>
}
value={value}
onUpdate={onUpdate}
/>
);
}
56 changes: 56 additions & 0 deletions src/ui/components/ColorPickerInput/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import chroma from 'chroma-js';

import {type ColorParts} from './ColorPickerInput';

export function normalizeColor(color?: string) {
return color?.toUpperCase() || '';
}

export function isEmptyColor(color?: string): color is undefined {
return color === '' || color === '#' || color === undefined;
}

export function colorMask(color?: string): string {
if (isEmptyColor(color)) {
return '';
} else {
return normalizeColor(`${color?.startsWith('#') ? '' : '#'}${color}`);
}
}

// Prevents browser warnings for empty\invalid values
export function sanitizeColor(color: string) {
return normalizeColor(chroma(color).hex());
}

export function getMaskedColor(color: ColorParts) {
return {...color, solid: colorMask(color.solid)};
}

export function isValidColor(color: string) {
return /^#[0-9a-f]{3}([0-9a-f]{3})?$/i.test(color);
}

export function getColorParts(rawValue?: string): ColorParts {
const chromaValue = rawValue && chroma.valid(rawValue, 'hex') ? chroma(rawValue) : undefined;

if (!chromaValue) {
return {solid: '', opacity: 100};
}

// const value = chromaValue.hex();
const opacity = chromaValue.alpha() * 100;
const solid = normalizeColor(chromaValue.alpha(1).hex('rgb'));

return {solid, opacity};
}

export function getResultColorFromParts(color: ColorParts): string {
if (!color.solid || !chroma.valid(color.solid, 'hex')) {
return '';
}

return chroma(color.solid)
.alpha(color.opacity === null ? 1 : color.opacity / 100)
.hex();
}
Loading

0 comments on commit 291ebfe

Please sign in to comment.