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:
Nafees Nazik
2023-02-17 20:27:29 +00:00
committed by GitHub
co-authored by Peer Richelsen
parent fd76bc9cab
commit 4f99746f68
6 changed files with 29 additions and 204 deletions
+1 -1
View File
@@ -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";
-102
View File
@@ -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);
-27
View File
@@ -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
View File
@@ -1 +0,0 @@
export { default as Swatch } from "./Swatch";
-1
View File
@@ -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 */