diff --git a/package-lock.json b/package-lock.json index 4bbe10cebb..93fd786b91 100644 --- a/package-lock.json +++ b/package-lock.json @@ -28,6 +28,7 @@ "@gravity-ui/i18n": "^1.7.0", "@gravity-ui/nodekit": "^2.4.1", "@node-rs/crc32": "^1.7.2", + "@types/chroma-js": "^3.1.1", "ajv": "^8.12.0", "axios": "^1.7.7", "axios-retry": "^3.9.1", @@ -11517,6 +11518,12 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/chroma-js": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@types/chroma-js/-/chroma-js-3.1.1.tgz", + "integrity": "sha512-SFCr4edNkZ1bGaLzGz7rgR1bRzVX4MmMxwsIa3/Bh6ose8v+hRpneoizHv0KChdjxaXyjRtaMq7sCuZSzPomQA==", + "license": "MIT" + }, "node_modules/@types/connect": { "version": "3.4.38", "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", diff --git a/package.json b/package.json index c616b5b102..b93b37dded 100644 --- a/package.json +++ b/package.json @@ -73,6 +73,7 @@ "@gravity-ui/i18n": "^1.7.0", "@gravity-ui/nodekit": "^2.4.1", "@node-rs/crc32": "^1.7.2", + "@types/chroma-js": "^3.1.1", "ajv": "^8.12.0", "axios": "^1.7.7", "axios-retry": "^3.9.1", diff --git a/src/i18n-keysets/wizard/en.json b/src/i18n-keysets/wizard/en.json index 18b934beb3..ec0ef37d27 100644 --- a/src/i18n-keysets/wizard/en.json +++ b/src/i18n-keysets/wizard/en.json @@ -271,6 +271,7 @@ "label_string": "String", "label_sub-totals-switcher": "Sub-totals", "label_sum": "Sum", + "label_text-color-mode": "Text color", "label_thresholds": "Set threshold values", "label_time-interval": "Time interval", "label_title": "Name", diff --git a/src/i18n-keysets/wizard/ru.json b/src/i18n-keysets/wizard/ru.json index 7a9f9793e9..70bc31ebc3 100644 --- a/src/i18n-keysets/wizard/ru.json +++ b/src/i18n-keysets/wizard/ru.json @@ -271,6 +271,7 @@ "label_string": "Строка", "label_sub-totals-switcher": "Подытоги", "label_sum": "Сумма", + "label_text-color-mode": "Цвет текста", "label_thresholds": "Задать пороговые значения", "label_time-interval": "Временной интервал", "label_title": "Название", diff --git a/src/server/modes/charts/plugins/datalens/preparers/flat-table/helpers/background-settings/get-flat-table-background-styles.ts b/src/server/modes/charts/plugins/datalens/preparers/flat-table/helpers/background-settings/get-flat-table-background-styles.ts index e6e02a6aa4..147364bd71 100644 --- a/src/server/modes/charts/plugins/datalens/preparers/flat-table/helpers/background-settings/get-flat-table-background-styles.ts +++ b/src/server/modes/charts/plugins/datalens/preparers/flat-table/helpers/background-settings/get-flat-table-background-styles.ts @@ -1,5 +1,6 @@ import type {MarkupItem} from '../../../../../../../../../shared'; import {getDistinctValue, markupToRawString} from '../../../../../../../../../shared'; +import {getColorBrightness} from '../../../../../../../../../shared/utils/color'; import {selectServerPalette} from '../../../../../../../../constants'; import {getColor} from '../../../../utils/constants'; import {findIndexInOrder} from '../../../../utils/misc-helpers'; @@ -68,7 +69,6 @@ const getDiscreteBackgroundColorStyle = (args: GetDiscreteBackgroundColorStyle) // eslint-disable-next-line consistent-return return { backgroundColor: colorValue, - color: '#FFF', }; }; @@ -106,29 +106,52 @@ export const getFlatTableBackgroundStyles = ( const backgroundSettings = column.backgroundSettings; if (!backgroundSettings) { - return; + return undefined; } const {settings} = backgroundSettings; + let backgroundCss: {backgroundColor?: string | number; color?: string} | undefined; + if (settings.isContinuous) { - // eslint-disable-next-line consistent-return - return getContinuousBackgroundColorStyle({ + backgroundCss = getContinuousBackgroundColorStyle({ backgroundColorsByMeasure, currentRowIndex, backgroundSettings, }); + } else { + backgroundCss = getDiscreteBackgroundColorStyle({ + column, + values, + backgroundSettings, + order, + idToTitle, + idToDataType, + loadedColorPalettes, + availablePalettes, + }); } - // eslint-disable-next-line consistent-return - return getDiscreteBackgroundColorStyle({ - column, - values, - backgroundSettings, - order, - idToTitle, - idToDataType, - loadedColorPalettes, - availablePalettes, - }); + const textColorSettings = backgroundSettings.textColor; + if (typeof backgroundCss?.backgroundColor === 'string') { + switch (textColorSettings?.mode) { + case 'manual': { + backgroundCss.color = textColorSettings.color; + break; + } + case 'auto': + default: { + const brightness = getColorBrightness(backgroundCss.backgroundColor); + const textColor = + brightness > 0.5 + ? 'var(--g-color-text-dark-primary)' + : 'var(--g-color-text-light-primary)'; + + backgroundCss.color = textColor; + break; + } + } + } + + return backgroundCss; }; diff --git a/src/shared/types/wizard/background-settings.ts b/src/shared/types/wizard/background-settings.ts index 50ca20cfde..8c8e040ae7 100644 --- a/src/shared/types/wizard/background-settings.ts +++ b/src/shared/types/wizard/background-settings.ts @@ -19,4 +19,9 @@ export interface TableFieldBackgroundSettings { gradientState: Pick; isContinuous: boolean; }; + textColor?: { + mode?: string; + color?: string; + palette?: string; + }; } diff --git a/src/shared/utils/color.ts b/src/shared/utils/color.ts new file mode 100644 index 0000000000..f23f47a7df --- /dev/null +++ b/src/shared/utils/color.ts @@ -0,0 +1,5 @@ +import chroma from 'chroma-js'; + +export function getColorBrightness(hex: string) { + return chroma(hex).luminance(); +} diff --git a/src/ui/units/wizard/components/Dialogs/DialogField/components/BackgroundSettings/BackgroundSettings.tsx b/src/ui/units/wizard/components/Dialogs/DialogField/components/BackgroundSettings/BackgroundSettings.tsx index 0618950e86..4517805aa4 100644 --- a/src/ui/units/wizard/components/Dialogs/DialogField/components/BackgroundSettings/BackgroundSettings.tsx +++ b/src/ui/units/wizard/components/Dialogs/DialogField/components/BackgroundSettings/BackgroundSettings.tsx @@ -3,6 +3,7 @@ import React from 'react'; import {SegmentedRadioGroup as RadioButton, Switch} from '@gravity-ui/uikit'; import block from 'bem-cn-lite'; import {i18n} from 'i18n'; +import {useSelector} from 'react-redux'; import type { ClientChartsConfig, Field, @@ -11,10 +12,12 @@ import type { TableFieldBackgroundSettings, WizardVisualizationId, } from 'shared'; -import {DialogFieldBackgroundSettingsQa} from 'shared'; +import {DEFAULT_PALETTE, DialogFieldBackgroundSettingsQa} from 'shared'; +import {selectColorPalettes} from 'ui/store/selectors/colorPaletteEditor'; import {NULLS_OPTIONS} from 'ui/units/wizard/constants/dialogColor'; import {DialogRadioButtons} from '../../../components/DialogRadioButtons/DialogRadioButtons'; +import {PaletteColorControl} from '../BarsSettings/components/PaletteColorControl/PaletteColorControl'; import {ButtonColorDialog} from '../ButtonColorDialog/ButtonColorDialog'; import {DialogFieldRow} from '../DialogFieldRow/DialogFieldRow'; import {DialogFieldSelect} from '../DialogFieldSelect/DialogFieldSelect'; @@ -43,6 +46,7 @@ type Props = { export const BackgroundSettings: React.FC = (props) => { const {state, onUpdate, visualization, currentField, placeholderId} = props; + const colorPalettes = useSelector(selectColorPalettes); const {field, datasetFieldsMap, chartFields, extraDistincts} = useBackgroundSettings({ visualization, @@ -81,6 +85,20 @@ export const BackgroundSettings: React.FC = (props) => { state, }); + const textColorMode = state.textColor?.mode ?? 'auto'; + const handleTextColorModeUpdate = React.useCallback( + (event: React.ChangeEvent) => { + onUpdate({ + ...state, + textColor: { + ...state?.textColor, + mode: event.target.value, + }, + }); + }, + [onUpdate, state], + ); + return (
= (props) => { } /> )} + + + {i18n('wizard', 'label_auto')} + + + {i18n('wizard', 'label_manual')} + + + } + /> + {textColorMode === 'manual' && ( + { + onUpdate({ + ...state, + textColor: { + ...state?.textColor, + color, + }, + }); + }} + onPaletteUpdate={(palette) => { + onUpdate({ + ...state, + textColor: { + ...state?.textColor, + palette, + }, + }); + }} + colorPalettes={colorPalettes} + /> + } + /> + )}
); }; diff --git a/src/ui/units/wizard/components/Dialogs/DialogField/components/BarsSettings/components/PaletteColorControl/PaletteColorControl.scss b/src/ui/units/wizard/components/Dialogs/DialogField/components/BarsSettings/components/PaletteColorControl/PaletteColorControl.scss index d1348fb551..07410ae4c5 100644 --- a/src/ui/units/wizard/components/Dialogs/DialogField/components/BarsSettings/components/PaletteColorControl/PaletteColorControl.scss +++ b/src/ui/units/wizard/components/Dialogs/DialogField/components/BarsSettings/components/PaletteColorControl/PaletteColorControl.scss @@ -12,8 +12,16 @@ margin-right: var(--g-spacing-1); margin-block: 2px; - &.palette-item:hover::before { - content: none; + &::before, + &.palette-item_selectable:hover::before { + width: 28px; + height: 28px; + content: ''; + position: absolute; + border-radius: 6px; + top: -2px; + left: -2px; + border: 1px solid var(--g-color-base-generic-medium); } } @@ -30,13 +38,13 @@ } &__palette { - z-index: 2; - border-radius: 8px; - box-shadow: 0 8px 20px 0 var(--g-color-sfx-shadow); - position: absolute; - top: 0; - left: 28px; - background-color: var(--g-color-base-background); + // z-index: 2; + // border-radius: 8px; + // box-shadow: 0 8px 20px 0 var(--g-color-sfx-shadow); + // position: absolute; + // top: 0; + // left: 28px; + // background-color: var(--g-color-base-background); padding: var(--g-spacing-4); } } diff --git a/src/ui/units/wizard/components/Dialogs/DialogField/components/BarsSettings/components/PaletteColorControl/PaletteColorControl.tsx b/src/ui/units/wizard/components/Dialogs/DialogField/components/BarsSettings/components/PaletteColorControl/PaletteColorControl.tsx index 1601fd7838..0f6515300d 100644 --- a/src/ui/units/wizard/components/Dialogs/DialogField/components/BarsSettings/components/PaletteColorControl/PaletteColorControl.tsx +++ b/src/ui/units/wizard/components/Dialogs/DialogField/components/BarsSettings/components/PaletteColorControl/PaletteColorControl.tsx @@ -1,10 +1,9 @@ import React, {useRef} from 'react'; -import {TextInput} from '@gravity-ui/uikit'; +import {Popup, TextInput} from '@gravity-ui/uikit'; import block from 'bem-cn-lite'; import {i18n} from 'i18n'; import type {ColorPalette} from 'shared'; -import {useOutsideClick} from 'ui/hooks/useOutsideClick'; import {MinifiedPalette} from 'ui/units/wizard/components/MinifiedPalette/MinifiedPalette'; import {isValidHexColor} from 'ui/utils'; @@ -14,12 +13,12 @@ import './PaletteColorControl.scss'; type PaletteColorControlProps = { palette: string; - controlQa: string; + controlQa?: string; currentColor: string; onPaletteItemChange: (color: string) => void; onPaletteUpdate: (paletteName: string) => void; - onError: (error: boolean) => void; - disabled: boolean; + onError?: (error: boolean) => void; + disabled?: boolean; colorPalettes: ColorPalette[]; }; @@ -44,10 +43,6 @@ export const PaletteColorControl: React.FC = ( const ref = useRef(null); - const handleOutsideClick = React.useCallback(() => { - setIsPaletteVisible(false); - }, []); - const handleInputColorUpdate = React.useCallback( (color: string) => { const hexColor = `#${color}`; @@ -55,11 +50,11 @@ export const PaletteColorControl: React.FC = ( if (!isValidHexColor(hexColor)) { setErrorText(i18n('wizard', 'label_bars-custom-color-error')); - onError(true); + onError?.(true); return; } - onError(false); + onError?.(false); setErrorText(''); }, [onError, onPaletteItemChange], @@ -78,12 +73,7 @@ export const PaletteColorControl: React.FC = ( setIsPaletteVisible(false); }, [currentColor, errorText]); - // Solves the problem of clicking on the palette in the selector. Since the palette list is rendered in the body, not in the ref container - const additionalCheck = React.useCallback(() => { - return Boolean(document.getElementsByClassName('g-select-list__item').length); - }, []); - - useOutsideClick(ref, handleOutsideClick, additionalCheck); + const paletteItemRef = React.useRef(null); return (
@@ -98,19 +88,19 @@ export const PaletteColorControl: React.FC = ( } }} qa={controlQa} + ref={paletteItemRef} /> - -
- {isPaletteVisible && ( -
+ { + if (reason === 'outside-press') { + setIsPaletteVisible(false); + } + }} + anchorElement={paletteItemRef.current} + open={isPaletteVisible} + placement={['right-end']} + className={b('palette')} + > = ( onEnterPress={handleEnterPress} colorPalettes={colorPalettes} /> -
- )} + + + ); }; diff --git a/src/ui/units/wizard/components/Palette/components/PaletteItem/PaletteItem.tsx b/src/ui/units/wizard/components/Palette/components/PaletteItem/PaletteItem.tsx index 080c9e7785..efc720ac26 100644 --- a/src/ui/units/wizard/components/Palette/components/PaletteItem/PaletteItem.tsx +++ b/src/ui/units/wizard/components/Palette/components/PaletteItem/PaletteItem.tsx @@ -16,32 +16,44 @@ type PaletteItemProps = { isSelected?: boolean; isDisabled?: boolean; isSelectable?: boolean; + children?: React.ReactNode; }; const b = block('palette-item'); -export const PaletteItem: React.FC> = ({ - children, - qa, - className, - onClick, - color, - isSelected, - isDefault, - isDisabled, - isSelectable = true, -}: React.PropsWithChildren) => { - const mods = { - default: Boolean(isDefault), - selected: Boolean(isSelected), - disabled: Boolean(isDisabled), - selectable: isSelectable, - }; - const style = color ? {backgroundColor: color} : undefined; - return ( -
- {isDisabled && } - {children} -
- ); -}; +export const PaletteItem = React.forwardRef( + ( + { + children, + qa, + className, + onClick, + color, + isSelected, + isDefault, + isDisabled, + isSelectable = true, + }: PaletteItemProps, + ref, + ) => { + const mods = { + default: Boolean(isDefault), + selected: Boolean(isSelected), + disabled: Boolean(isDisabled), + selectable: isSelectable, + }; + const style = color ? {backgroundColor: color} : undefined; + return ( +
+ {isDisabled && } + {children} +
+ ); + }, +);