mirror of
https://github.com/webstudio-is/webstudio.git
synced 2026-11-09 05:26:24 +09:00
fix: fix color control spacing, border and accessibility (#4089)
Closes https://github.com/webstudio-is/webstudio/issues/1244 ## Steps for reproduction 1. see even outer spacing <img width="53" alt="image" src="https://github.com/user-attachments/assets/39967869-4ada-4bd6-a687-7f465e64f277"> 2. see border appears when color nears white but doesn't show up when color is far from white 3. See that you can focus color swatch with keyboard arrows 4. See that keyboard enter opens the color picker 5. See that transparent chessboard pattern still works ## Code Review - [ ] hi @kof, I need you to do - conceptual review (architecture, feature-correctness) - detailed review (read every line) - test it on preview ## Before requesting a review - [ ] made a self-review - [ ] added inline comments where things may be not obvious (the "why", not "what") ## Before merging - [ ] tested locally and on preview environment (preview dev login: 5de6) - [ ] updated [test cases](https://github.com/webstudio-is/webstudio/blob/main/apps/builder/docs/test-cases.md) document - [ ] added tests - [ ] if any new env variables are added, added them to `.env` file
This commit is contained in:
1 parent
ff9630b07a
commit
3d6d2ad4be
4 files changed
+51
-40
No files matched your search
@@ -218,7 +218,8 @@ export const ColorPicker = ({
|
||||
>
|
||||
<ColorThumb
|
||||
color={styleValueToRgbaColor(currentColor)}
|
||||
css={{ margin: theme.spacing[3] }}
|
||||
css={{ margin: theme.spacing[2] }}
|
||||
tabIndex={-1}
|
||||
/>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
import { rawTheme, Box, theme, css } from "@webstudio-is/design-system";
|
||||
import { rawTheme, theme, css, type CSS } from "@webstudio-is/design-system";
|
||||
import { colord, type RgbaColor } from "colord";
|
||||
import { forwardRef, type ElementRef, type ComponentProps } from "react";
|
||||
import { clamp } from "~/shared/math-utils";
|
||||
|
||||
const whiteColor = { r: 255, g: 255, b: 255, a: 1 };
|
||||
const whiteColor: RgbaColor = { r: 255, g: 255, b: 255, a: 1 };
|
||||
const borderColorSwatch = colord(rawTheme.colors.borderColorSwatch).toRgb();
|
||||
const transparent = colord("transparent").toRgb();
|
||||
const transparentColor: RgbaColor = { r: 0, g: 0, b: 0, a: 0 };
|
||||
|
||||
const distance = (a: RgbaColor, b: RgbaColor) => {
|
||||
// Use Euclidian distance, if will not give good results use https://zschuessler.github.io/DeltaE/
|
||||
// Use Euclidian distance
|
||||
// If results are not good, lets switch to https://zschuessler.github.io/DeltaE/
|
||||
return Math.sqrt(
|
||||
Math.pow(a.r / 255 - b.r / 255, 2) +
|
||||
Math.pow(a.g / 255 - b.g / 255, 2) +
|
||||
@@ -17,6 +18,19 @@ const distance = (a: RgbaColor, b: RgbaColor) => {
|
||||
);
|
||||
};
|
||||
|
||||
// White color is invisible on white background, so we need to draw border
|
||||
// the more color is white the more border is visible
|
||||
const calcBorderColor = (color: RgbaColor) => {
|
||||
const distanceToStartDrawBorder = 0.15;
|
||||
const alpha = clamp(
|
||||
(distanceToStartDrawBorder - distance(whiteColor, color)) /
|
||||
distanceToStartDrawBorder,
|
||||
0,
|
||||
1
|
||||
);
|
||||
return colord(lerpColor(transparentColor, borderColorSwatch, alpha));
|
||||
};
|
||||
|
||||
const lerp = (a: number, b: number, t: number) => {
|
||||
return a * (1 - t) + b * t;
|
||||
};
|
||||
@@ -30,53 +44,37 @@ const lerpColor = (a: RgbaColor, b: RgbaColor, t: number) => {
|
||||
};
|
||||
};
|
||||
|
||||
const style = css({
|
||||
const thumbStyle = css({
|
||||
width: theme.spacing[10],
|
||||
height: theme.spacing[10],
|
||||
backgroundBlendMode: "difference",
|
||||
borderRadius: 2,
|
||||
borderRadius: theme.borderRadius[2],
|
||||
borderWidth: 0,
|
||||
borderStyle: "solid",
|
||||
});
|
||||
|
||||
export const ColorThumb = forwardRef<
|
||||
ElementRef<typeof Box>,
|
||||
Omit<ComponentProps<typeof Box>, "color"> & { color: RgbaColor | undefined }
|
||||
>(({ color, css, ...rest }, ref) => {
|
||||
const colorString = colord(color || transparent).toRgbString();
|
||||
// @todo transparent icon can be better
|
||||
ElementRef<"button">,
|
||||
Omit<ComponentProps<"button">, "color"> & { color?: RgbaColor; css?: CSS }
|
||||
>(({ color = transparentColor, css, ...rest }, ref) => {
|
||||
const background =
|
||||
color === null || (color && color.a < 1)
|
||||
? // chessboard 5x5
|
||||
`repeating-conic-gradient(rgba(0,0,0,0.22) 0% 25%, transparent 0% 50%) 0% 33.33% / 40% 40%, ${colorString}`
|
||||
: colorString;
|
||||
|
||||
const distanceToStartDrawBorder = 0.15;
|
||||
|
||||
// White color is invisible on white background, so we need to draw border
|
||||
// the more color is white the more border is visible
|
||||
const borderColor = colord(
|
||||
lerpColor(
|
||||
transparent,
|
||||
borderColorSwatch,
|
||||
clamp(
|
||||
(distanceToStartDrawBorder -
|
||||
distance(whiteColor, color || transparent)) /
|
||||
distanceToStartDrawBorder,
|
||||
0,
|
||||
1
|
||||
)
|
||||
)
|
||||
).toRgbString();
|
||||
color === undefined || color.a < 1
|
||||
? // Chessboard pattern 5x5
|
||||
`repeating-conic-gradient(rgba(0,0,0,0.22) 0% 25%, transparent 0% 50%) 0% 33.33% / 40% 40%, ${colord(color).toRgbString()}`
|
||||
: colord(color).toRgbString();
|
||||
const borderColor = calcBorderColor(color);
|
||||
|
||||
return (
|
||||
<Box
|
||||
<button
|
||||
{...rest}
|
||||
ref={ref}
|
||||
css={{
|
||||
style={{
|
||||
background,
|
||||
borderColor,
|
||||
borderColor: borderColor.toRgbString(),
|
||||
// Border becomes visible when color is close to white so that the thumb is visible in the white input.
|
||||
borderWidth: borderColor.alpha() === 0 ? 0 : 1,
|
||||
}}
|
||||
className={style({ css })}
|
||||
className={thumbStyle({ css })}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -280,6 +280,8 @@ export const InputField = forwardRef(
|
||||
// This way user can unfocus the input and then use any single-key shortcut.
|
||||
tabIndex={-1}
|
||||
ref={unfocusContainerRef}
|
||||
// When managing focus with ArrowFocus, we don't want to focus this element.
|
||||
data-no-arrow-focus
|
||||
/>
|
||||
<Input {...inputProps} onKeyDown={handleKeyDown} ref={inputRef} />
|
||||
</Container>
|
||||
|
||||
@@ -51,6 +51,12 @@ const willEventMoveCaret = (event: KeyboardEvent) => {
|
||||
return true;
|
||||
};
|
||||
|
||||
const accept = (element: Element) => {
|
||||
// In some cases we want to have an element that is tabbable, but it should not be ignored for arrow focus management.
|
||||
// One use case is in input field, which is using ESC to focus an div to unfocus the input
|
||||
return element.hasAttribute("data-no-arrow-focus") === false;
|
||||
};
|
||||
|
||||
// Need this wrapper becuase we can't call useFocusManager
|
||||
// in the same component that renders FocusScope
|
||||
const ContextHelper = ({ render }: { render: Render }) => {
|
||||
@@ -79,11 +85,15 @@ const ContextHelper = ({ render }: { render: Render }) => {
|
||||
}
|
||||
|
||||
if (event.key === "ArrowRight" || event.key === "ArrowDown") {
|
||||
focusManager.focusNext({ wrap: true, ...focusManagerOptions });
|
||||
focusManager.focusNext({ wrap: true, accept, ...focusManagerOptions });
|
||||
event.preventDefault(); // Prevents the page from scrolling
|
||||
}
|
||||
if (event.key === "ArrowLeft" || event.key === "ArrowUp") {
|
||||
focusManager.focusPrevious({ wrap: true, ...focusManagerOptions });
|
||||
focusManager.focusPrevious({
|
||||
wrap: true,
|
||||
accept,
|
||||
...focusManagerOptions,
|
||||
});
|
||||
event.preventDefault(); // Prevents the page from scrolling
|
||||
}
|
||||
},
|
||||
|
||||
Reference in new issue
Block a user