Migrate the color picker and color selector off Emotion (#83480)

* Migrate the color picker and color selector off Emotion

* Move the color picker styles into its classes
This commit is contained in:
Andrei Chernykh
2026-10-02 15:03:14 -07:00
committed by GitHub
parent cdd2788f2a
commit dff4d17f72
6 changed files with 73 additions and 102 deletions

View File

@@ -0,0 +1,49 @@
/*
* react-color injects these gradients through <style> elements without a nonce, which our CSP blocks, so they are copied here.
* https://github.com/casesandberg/react-color/blob/v2.18.1/src/components/common/Hue.js#L78
* https://github.com/casesandberg/react-color/blob/v2.18.1/src/components/common/Saturation.js#L94
*/
.controls :global(.hue-horizontal) {
background: linear-gradient(
to right,
#f00 0%,
#ff0 17%,
#0f0 33%,
#0ff 50%,
#00f 67%,
#f0f 83%,
#f00 100%
);
}
.controls :global(.saturation-white) {
background: linear-gradient(to right, #fff, rgba(255, 255, 255, 0));
}
.controls :global(.saturation-black) {
background: linear-gradient(to top, #000, rgba(0, 0, 0, 0));
}
.track {
position: relative;
overflow: hidden;
border-radius: 0.25rem;
}
.pointer {
border: 0.125rem solid var(--mb-color-background_page-primary);
border-radius: 50%;
pointer-events: none;
}
.saturationPointer {
width: 0.875rem;
height: 0.875rem;
transform: translate(-50%, -50%);
}
.huePointer {
width: 0.625rem;
height: 0.625rem;
transform: translate(-50%, -0.0625rem);
}

View File

@@ -1,71 +0,0 @@
// eslint-disable-next-line no-restricted-imports
import styled from "@emotion/styled";
export const ContentContainer = styled.div`
display: flex;
flex-direction: column;
gap: 1rem;
width: 16.5rem;
padding: 1rem;
`;
// react-css inserts inline styles, but it's forbidden with our CSP headers
// to fix this we copy relevant styles here
// https://github.com/casesandberg/react-color/blob/v2.18.1/src/components/common/Hue.js#L78
// https://github.com/casesandberg/react-color/blob/v2.18.1/src/components/common/Saturation.js#L94
// eslint-disable-next-line metabase/no-color-literals
export const ControlsRoot = styled.div`
.hue-horizontal {
background: linear-gradient(
to right,
#f00 0%,
#ff0 17%,
#0f0 33%,
#0ff 50%,
#00f 67%,
#f0f 83%,
#f00 100%
);
}
.saturation-white {
background: linear-gradient(to right, #fff, rgba(255, 255, 255, 0));
}
.saturation-black {
background: linear-gradient(to top, #000, rgba(0, 0, 0, 0));
}
`;
export const SaturationContainer = styled.div`
position: relative;
height: 10rem;
margin-bottom: 1rem;
border-radius: 0.25rem;
overflow: hidden;
`;
export const HueContainer = styled.div`
position: relative;
height: 0.5rem;
border-radius: 0.25rem;
overflow: hidden;
`;
const ControlsPointer = styled.div`
border: 2px solid var(--mb-color-background_page-primary);
border-radius: 50%;
pointer-events: none;
`;
export const SaturationPointer = styled(ControlsPointer)`
width: 0.875rem;
height: 0.875rem;
transform: translate(-50%, -50%);
`;
export const HuePointer = styled(ControlsPointer)`
width: 0.625rem;
height: 0.625rem;
transform: translate(-50%, -0.0625rem);
`;

View File

@@ -5,9 +5,8 @@ import type { ColorState } from "react-color";
import { t } from "ttag";
import { ColorInput } from "metabase/common/components/ColorInput";
import { Group, NumberInput } from "metabase/ui";
import { Group, NumberInput, Stack } from "metabase/ui";
import { ContentContainer } from "./ColorPicker.styled";
import { ColorPickerControls } from "./ColorPickerControls";
export type ColorPickerContentAttributes = Omit<
@@ -64,7 +63,7 @@ export const ColorPickerContent = forwardRef(function ColorPickerContent(
);
return (
<ContentContainer {...props} ref={ref}>
<Stack {...props} ref={ref} gap="lg" w="16.5rem" p="lg">
<ColorPickerControls color={value} onChange={handlePickerChange} />
{showAlpha ? (
<Group gap="sm" wrap="nowrap" align="flex-start">
@@ -83,7 +82,7 @@ export const ColorPickerContent = forwardRef(function ColorPickerContent(
) : (
<ColorInput value={value} fullWidth onChange={onChange} />
)}
</ContentContainer>
</Stack>
);
});

View File

@@ -1,14 +1,11 @@
import cx from "classnames";
import type { CustomPickerInjectedProps } from "react-color";
import { CustomPicker } from "react-color";
import { Hue, Saturation } from "react-color/lib/components/common";
import {
ControlsRoot,
HueContainer,
HuePointer,
SaturationContainer,
SaturationPointer,
} from "./ColorPicker.styled";
import { Box } from "metabase/ui";
import S from "./ColorPicker.module.css";
const saturationStyles = {
color: {
@@ -21,17 +18,25 @@ export const ColorPickerControls = CustomPicker(function ColorControls(
props: CustomPickerInjectedProps,
) {
return (
<ControlsRoot>
<SaturationContainer>
<Box className={S.controls}>
<Box className={S.track} h="10rem" mb="lg">
<Saturation
{...props}
pointer={SaturationPointer}
style={saturationStyles}
/>
</SaturationContainer>
<HueContainer>
</Box>
<Box className={S.track} h="0.5rem">
<Hue {...props} pointer={HuePointer} />
</HueContainer>
</ControlsRoot>
</Box>
</Box>
);
});
function SaturationPointer() {
return <Box className={cx(S.pointer, S.saturationPointer)} />;
}
function HuePointer() {
return <Box className={cx(S.pointer, S.huePointer)} />;
}

View File

@@ -1,10 +0,0 @@
// eslint-disable-next-line no-restricted-imports
import styled from "@emotion/styled";
export const PopoverRoot = styled.div`
display: flex;
flex-wrap: wrap;
gap: 0.25rem;
padding: 0.75rem;
max-width: 22rem;
`;

View File

@@ -2,8 +2,7 @@ import type { HTMLAttributes, Ref } from "react";
import { forwardRef, useCallback } from "react";
import { ColorPill } from "metabase/common/components/ColorPill";
import { PopoverRoot } from "./ColorSelectorPopover.styled";
import { Flex } from "metabase/ui";
/**
* A picker given named palette colors reports which one was chosen, so the
@@ -44,7 +43,7 @@ export const ColorSelectorPopover = forwardRef(function ColorSelector(
);
return (
<PopoverRoot {...props} ref={ref}>
<Flex {...props} ref={ref} wrap="wrap" gap="xxs" p="md" maw="22rem">
{colors.map(toNamedColor).map((option, index) => (
<ColorPill
key={index}
@@ -53,6 +52,6 @@ export const ColorSelectorPopover = forwardRef(function ColorSelector(
onSelect={(newValue) => handleSelect(newValue, option.name)}
/>
))}
</PopoverRoot>
</Flex>
);
});