chore: Extract SwatchButton from InputColor (#10693)

This commit is contained in:
Tom Moor
2025-11-23 14:28:05 +01:00
committed by GitHub
parent a9263afa2c
commit 8619ef2bea
3 changed files with 117 additions and 70 deletions

View File

@@ -1,85 +1,40 @@
import * as React from "react";
import { useTranslation } from "react-i18next";
import styled from "styled-components";
import { s } from "@shared/styles";
import lazyWithRetry from "~/utils/lazyWithRetry";
import DelayedMount from "./DelayedMount";
import Input, { Props as InputProps } from "./Input";
import NudeButton from "./NudeButton";
import Relative from "./Sidebar/components/Relative";
import Text from "./Text";
import { Popover, PopoverContent, PopoverTrigger } from "./primitives/Popover";
import { SwatchButton } from "./SwatchButton";
/**
* Props for the InputColor component.
*/
type Props = Omit<InputProps, "onChange"> & {
/** The current color value in hex format */
value: string | undefined;
/** Callback function invoked when the color value changes */
onChange: (value: string) => void;
};
const InputColor: React.FC<Props> = ({ value, onChange, ...rest }: Props) => {
const { t } = useTranslation();
/**
* A color input component that combines a text input with a color picker swatch button.
* Automatically formats hex color values with a leading # character.
*/
const InputColor: React.FC<Props> = ({ value, onChange, ...rest }: Props) => (
<Relative>
<Input
value={value}
onChange={(event) => onChange(event.target.value.replace(/^#?/, "#"))}
placeholder="#"
maxLength={7}
{...rest}
/>
<PositionedSwatchButton color={value} onChange={onChange} />
</Relative>
);
return (
<Relative>
<Input
value={value}
onChange={(event) => onChange(event.target.value.replace(/^#?/, "#"))}
placeholder="#"
maxLength={7}
{...rest}
/>
<Popover modal={true}>
<PopoverTrigger>
<SwatchButton aria-label={t("Show menu")} $background={value} />
</PopoverTrigger>
<StyledContent aria-label={t("Select a color")} align="end">
<React.Suspense
fallback={
<DelayedMount>
<Text>{t("Loading")}</Text>
</DelayedMount>
}
>
<StyledColorPicker
disableAlpha
color={value}
onChange={(color) => onChange(color.hex)}
/>
</React.Suspense>
</StyledContent>
</Popover>
</Relative>
);
};
const SwatchButton = styled(NudeButton)<{ $background: string | undefined }>`
background: ${(props) => props.$background};
border: 1px solid ${s("inputBorder")};
border-radius: 50%;
const PositionedSwatchButton = styled(SwatchButton)`
position: absolute;
bottom: 20px;
right: 6px;
`;
const StyledContent = styled(PopoverContent)`
width: auto;
padding: 8px;
`;
const ColorPicker = lazyWithRetry(
() => import("react-color/lib/components/chrome/Chrome")
);
const StyledColorPicker = styled(ColorPicker)`
background: inherit !important;
box-shadow: none !important;
border: 0 !important;
border-radius: 0 !important;
user-select: none;
input {
user-select: text;
color: ${s("text")} !important;
}
`;
export default InputColor;

View File

@@ -0,0 +1,92 @@
import * as React from "react";
import { useTranslation } from "react-i18next";
import styled from "styled-components";
import { s } from "@shared/styles";
import lazyWithRetry from "~/utils/lazyWithRetry";
import DelayedMount from "./DelayedMount";
import NudeButton from "./NudeButton";
import { Popover, PopoverTrigger, PopoverContent } from "./primitives/Popover";
import Text from "./Text";
/**
* Props for the SwatchButton component.
*/
type SwatchButtonProps = {
/** The current color value in hex format */
color?: string;
/** Callback function invoked when the color is changed */
onChange: (color: string) => void;
/** Additional CSS class name to apply to the button */
className?: string;
/** Whether to render the color picker in a modal popover. Defaults to true */
pickerInModal?: boolean;
};
export const SwatchButton: React.FC<SwatchButtonProps> = ({
color,
onChange,
className,
pickerInModal = true,
}) => {
const { t } = useTranslation();
return (
<Popover modal={pickerInModal}>
<PopoverTrigger>
<StyledSwatchButton
aria-label={t("Select a color")}
className={className}
style={{ background: color }}
/>
</PopoverTrigger>
<StyledContent
side="bottom"
align="end"
aria-label={t("Select a color")}
shrink
>
<React.Suspense
fallback={
<DelayedMount>
<Text>{t("Loading")}</Text>
</DelayedMount>
}
>
<StyledColorPicker
disableAlpha
color={color}
onChange={(c) => onChange(c.hex)}
/>
</React.Suspense>
</StyledContent>
</Popover>
);
};
const StyledSwatchButton = styled(NudeButton)`
background: ${s("menuBackground")};
border: 1px solid ${s("inputBorder")};
border-radius: 50%;
`;
const StyledContent = styled(PopoverContent)`
width: auto;
padding: 8px;
`;
const ColorPicker = lazyWithRetry(
() => import("react-color/lib/components/chrome/Chrome")
);
const StyledColorPicker = styled(ColorPicker)`
background: inherit !important;
box-shadow: none !important;
border: 0 !important;
border-radius: 0 !important;
user-select: none;
input {
user-select: text;
color: ${s("text")} !important;
}
`;

View File

@@ -314,8 +314,6 @@
"Objects": "Objects",
"Symbols": "Symbols",
"Flags": "Flags",
"Select a color": "Select a color",
"Loading": "Loading",
"View only": "View only",
"Can edit": "Can edit",
"No access": "No access",
@@ -450,6 +448,8 @@
"Installation": "Installation",
"Unstar document": "Unstar document",
"Star document": "Star document",
"Select a color": "Select a color",
"Loading": "Loading",
"Template created, go ahead and customize it": "Template created, go ahead and customize it",
"Creating a template from <em>{{titleWithDefault}}</em> is a non-destructive action we'll make a copy of the document and turn it into a template that can be used as a starting point for new documents.": "Creating a template from <em>{{titleWithDefault}}</em> is a non-destructive action we'll make a copy of the document and turn it into a template that can be used as a starting point for new documents.",
"Enable other members to use the template immediately": "Enable other members to use the template immediately",