mirror of
https://github.com/outline/outline.git
synced 2026-01-07 11:40:08 -06:00
chore: Extract SwatchButton from InputColor (#10693)
This commit is contained in:
@@ -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;
|
||||
|
||||
92
app/components/SwatchButton.tsx
Normal file
92
app/components/SwatchButton.tsx
Normal 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;
|
||||
}
|
||||
`;
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user