fix: color picker a11y fixes (#7084)
* refactor: remove swatch component * feat: use radix ui popover component * refactor: remove duplicate component * fix: import color picker from `@calcom/ui` * fix: type error * fix: update color picker to match the design * fix: type --------- Co-authored-by: Peer Richelsen <peer@cal.com>
This commit is contained in:
co-authored by
Peer Richelsen
parent
fd76bc9cab
commit
4f99746f68
@@ -20,10 +20,10 @@ import {
|
||||
Switch,
|
||||
TextArea,
|
||||
TextField,
|
||||
ColorPicker,
|
||||
} from "@calcom/ui";
|
||||
import { FiCode, FiTrello, FiSun, FiArrowLeft, FiChevronRight } from "@calcom/ui/components/icon";
|
||||
|
||||
import ColorPicker from "@components/ui/colorpicker";
|
||||
import Select from "@components/ui/form/Select";
|
||||
|
||||
type EmbedType = "inline" | "floating-popup" | "element-click";
|
||||
|
||||
@@ -1,102 +0,0 @@
|
||||
import { useCallback, useRef, useState } from "react";
|
||||
import { useEffect } from "react";
|
||||
import { HexColorInput, HexColorPicker } from "react-colorful";
|
||||
|
||||
import { isValidHexCode, fallBackHex } from "@calcom/lib/CustomBranding";
|
||||
import { Swatch } from "@calcom/ui";
|
||||
|
||||
type Handler = (event: MouseEvent | Event) => void;
|
||||
function useEventListener<
|
||||
KW extends keyof WindowEventMap,
|
||||
KH extends keyof HTMLElementEventMap,
|
||||
T extends HTMLElement | void = void
|
||||
>(
|
||||
eventName: KW | KH,
|
||||
handler: (event: WindowEventMap[KW] | HTMLElementEventMap[KH] | Event) => void,
|
||||
element?: React.RefObject<T>
|
||||
) {
|
||||
// Create a ref that stores handler
|
||||
const savedHandler = useRef<typeof handler>();
|
||||
useEffect(() => {
|
||||
// Define the listening target
|
||||
const targetElement: T | Window = element?.current || window;
|
||||
if (!(targetElement && targetElement.addEventListener)) {
|
||||
return;
|
||||
}
|
||||
// Update saved handler if necessary
|
||||
if (savedHandler.current !== handler) {
|
||||
savedHandler.current = handler;
|
||||
}
|
||||
// Create event listener that calls handler function stored in ref
|
||||
const eventListener: typeof handler = (event) => {
|
||||
// eslint-disable-next-line no-extra-boolean-cast
|
||||
if (!!savedHandler?.current) {
|
||||
savedHandler.current(event);
|
||||
}
|
||||
};
|
||||
targetElement.addEventListener(eventName, eventListener);
|
||||
// Remove event listener on cleanup
|
||||
return () => {
|
||||
targetElement.removeEventListener(eventName, eventListener);
|
||||
};
|
||||
}, [eventName, element, handler]);
|
||||
}
|
||||
|
||||
function useOnClickOutside<T extends HTMLElement = HTMLElement>(
|
||||
ref: React.RefObject<T>,
|
||||
handler: Handler,
|
||||
mouseEvent: "mousedown" | "mouseup" = "mousedown"
|
||||
): void {
|
||||
useEventListener(mouseEvent, (event) => {
|
||||
const el = ref?.current;
|
||||
// Do nothing if clicking ref's element or descendent elements
|
||||
if (!el || el.contains(event.target as Node)) {
|
||||
return;
|
||||
}
|
||||
handler(event);
|
||||
});
|
||||
}
|
||||
export type ColorPickerProps = {
|
||||
defaultValue: string;
|
||||
onChange: (text: string) => void;
|
||||
};
|
||||
|
||||
const ColorPicker = (props: ColorPickerProps) => {
|
||||
const init = !isValidHexCode(props.defaultValue)
|
||||
? fallBackHex(props.defaultValue, false)
|
||||
: props.defaultValue;
|
||||
const [color, setColor] = useState(init);
|
||||
const [isOpen, toggle] = useState(false);
|
||||
const popover = useRef() as React.MutableRefObject<HTMLInputElement>;
|
||||
const close = useCallback(() => toggle(false), []);
|
||||
useOnClickOutside(popover, close);
|
||||
return (
|
||||
<div className="relative mt-1 flex items-center justify-center">
|
||||
<Swatch size="sm" backgroundColor={color} onClick={() => toggle(!isOpen)} />
|
||||
|
||||
{isOpen && (
|
||||
<div className="popover" ref={popover}>
|
||||
<HexColorPicker
|
||||
className="!absolute !top-10 !left-0 !z-10 !h-32 !w-32"
|
||||
color={color}
|
||||
onChange={(val) => {
|
||||
setColor(val);
|
||||
props.onChange(val);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<HexColorInput
|
||||
className="ml-1 block w-full rounded-r-md border border-gray-300 px-3 py-2 sm:text-sm"
|
||||
color={color}
|
||||
onChange={(val) => {
|
||||
setColor(val);
|
||||
props.onChange(val);
|
||||
}}
|
||||
type="text"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ColorPicker;
|
||||
@@ -1,61 +1,9 @@
|
||||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
import * as Popover from "@radix-ui/react-popover";
|
||||
import { useState } from "react";
|
||||
import { HexColorInput, HexColorPicker } from "react-colorful";
|
||||
|
||||
import { fallBackHex, isValidHexCode } from "@calcom/lib/CustomBranding";
|
||||
|
||||
import { Swatch } from "../../..";
|
||||
|
||||
type Handler = (event: MouseEvent | Event) => void;
|
||||
function useEventListener<
|
||||
KW extends keyof WindowEventMap,
|
||||
KH extends keyof HTMLElementEventMap,
|
||||
T extends HTMLElement | void = void
|
||||
>(
|
||||
eventName: KW | KH,
|
||||
handler: (event: WindowEventMap[KW] | HTMLElementEventMap[KH] | Event) => void,
|
||||
element?: React.RefObject<T>
|
||||
) {
|
||||
// Create a ref that stores handler
|
||||
const savedHandler = useRef<typeof handler>();
|
||||
useEffect(() => {
|
||||
// Define the listening target
|
||||
const targetElement: T | Window = element?.current || window;
|
||||
if (!(targetElement && targetElement.addEventListener)) {
|
||||
return;
|
||||
}
|
||||
// Update saved handler if necessary
|
||||
if (savedHandler.current !== handler) {
|
||||
savedHandler.current = handler;
|
||||
}
|
||||
// Create event listener that calls handler function stored in ref
|
||||
const eventListener: typeof handler = (event) => {
|
||||
// eslint-disable-next-line no-extra-boolean-cast
|
||||
if (!!savedHandler?.current) {
|
||||
savedHandler.current(event);
|
||||
}
|
||||
};
|
||||
targetElement.addEventListener(eventName, eventListener);
|
||||
// Remove event listener on cleanup
|
||||
return () => {
|
||||
targetElement.removeEventListener(eventName, eventListener);
|
||||
};
|
||||
}, [eventName, element, handler]);
|
||||
}
|
||||
|
||||
function useOnClickOutside<T extends HTMLElement = HTMLElement>(
|
||||
ref: React.RefObject<T>,
|
||||
handler: Handler,
|
||||
mouseEvent: "mousedown" | "mouseup" = "mousedown"
|
||||
): void {
|
||||
useEventListener(mouseEvent, (event) => {
|
||||
const el = ref?.current;
|
||||
// Do nothing if clicking ref's element or descendent elements
|
||||
if (!el || el.contains(event.target as Node)) {
|
||||
return;
|
||||
}
|
||||
handler(event);
|
||||
});
|
||||
}
|
||||
export type ColorPickerProps = {
|
||||
defaultValue: string;
|
||||
onChange: (text: string) => void;
|
||||
@@ -66,27 +14,35 @@ const ColorPicker = (props: ColorPickerProps) => {
|
||||
? fallBackHex(props.defaultValue, false)
|
||||
: props.defaultValue;
|
||||
const [color, setColor] = useState(init);
|
||||
const [isOpen, toggle] = useState(false);
|
||||
const popover = useRef() as React.MutableRefObject<HTMLInputElement>;
|
||||
const close = useCallback(() => toggle(false), []);
|
||||
useOnClickOutside(popover, close);
|
||||
|
||||
return (
|
||||
<div className="relative mt-1 flex items-center justify-center">
|
||||
<Swatch size="sm" backgroundColor={color} onClick={() => toggle(!isOpen)} />
|
||||
{isOpen && (
|
||||
<div className="popover" ref={popover}>
|
||||
<HexColorPicker
|
||||
className="!absolute !top-10 !left-0 !z-10 !h-32 !w-32"
|
||||
color={color}
|
||||
onChange={(val) => {
|
||||
setColor(val);
|
||||
props.onChange(val);
|
||||
}}
|
||||
/>
|
||||
<div className=" mt-1 flex items-center justify-center">
|
||||
<Popover.Root>
|
||||
<div className="flex items-center rounded-l-md border border-r-0 border-gray-300 p-1.5">
|
||||
<Popover.Trigger asChild>
|
||||
<button
|
||||
className="h-6 w-6 rounded-sm border-2 border-gray-900"
|
||||
aria-label="pick colors"
|
||||
style={{ backgroundColor: color }}
|
||||
/>
|
||||
</Popover.Trigger>
|
||||
</div>
|
||||
)}
|
||||
<Popover.Portal>
|
||||
<Popover.Content align="center" sideOffset={10}>
|
||||
<HexColorPicker
|
||||
color={color}
|
||||
className="!h-32 !w-32"
|
||||
onChange={(val) => {
|
||||
setColor(val);
|
||||
props.onChange(val);
|
||||
}}
|
||||
/>
|
||||
</Popover.Content>
|
||||
</Popover.Portal>
|
||||
</Popover.Root>
|
||||
|
||||
<HexColorInput
|
||||
className="block w-full border border-gray-300 px-3 py-2 ltr:ml-1 ltr:rounded-r-md rtl:mr-1 rtl:rounded-l-md sm:text-sm"
|
||||
className="block w-full border border-gray-300 px-3 py-2 ltr:rounded-r-md rtl:rounded-l-md sm:text-sm"
|
||||
color={color}
|
||||
onChange={(val) => {
|
||||
setColor(val);
|
||||
|
||||
@@ -1,27 +0,0 @@
|
||||
import classNames from "@calcom/lib/classNames";
|
||||
|
||||
export type SwatchProps = {
|
||||
size?: "base" | "sm" | "lg";
|
||||
backgroundColor: string;
|
||||
onClick: () => void;
|
||||
};
|
||||
|
||||
const Swatch = (props: SwatchProps) => {
|
||||
const { size, backgroundColor, onClick } = props;
|
||||
return (
|
||||
<div className="border-2 border-gray-200 p-1">
|
||||
<div
|
||||
onClick={onClick}
|
||||
style={{ backgroundColor }}
|
||||
className={classNames(
|
||||
"cursor-pointer",
|
||||
size === "sm" && "h-6 w-6 rounded-sm",
|
||||
size === "base" && "h-16 w-16 rounded-sm",
|
||||
size === "lg" && "h-24 w-24 rounded-sm"
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Swatch;
|
||||
@@ -1 +0,0 @@
|
||||
export { default as Swatch } from "./Swatch";
|
||||
@@ -109,7 +109,6 @@ export {
|
||||
export type { DialogProps, ConfirmationDialogContentProps } from "./components/dialog";
|
||||
export { showToast } from "./components/toast"; // We don't export the toast components as they are only used in local storybook file
|
||||
export { Meta, MetaProvider, useMeta } from "./components/meta";
|
||||
export { Swatch } from "./components/swatch";
|
||||
export { ShellSubHeading } from "./components/layout";
|
||||
|
||||
/** ⬇️ TODO - Move these to components */
|
||||
|
||||
Reference in New Issue
Block a user