feat: Keyboard navigation spreadsheet layout for issues (#3564)
* enable keyboard navigation for spreadsheet layout * move the logic to table level instead of cell level * fix perf issue that made it unusable * fix scroll issue with navigation * fix build errors
This commit is contained in:
parent
a43dfc097d
commit
fb3dd77b66
31 changed files with 368 additions and 126 deletions
|
|
@ -15,6 +15,7 @@ const CustomMenu = (props: ICustomMenuDropdownProps) => {
|
|||
const {
|
||||
buttonClassName = "",
|
||||
customButtonClassName = "",
|
||||
customButtonTabIndex = 0,
|
||||
placement,
|
||||
children,
|
||||
className = "",
|
||||
|
|
@ -29,6 +30,7 @@ const CustomMenu = (props: ICustomMenuDropdownProps) => {
|
|||
verticalEllipsis = false,
|
||||
portalElement,
|
||||
menuButtonOnClick,
|
||||
onMenuClose,
|
||||
tabIndex,
|
||||
closeOnSelect,
|
||||
} = props;
|
||||
|
|
@ -47,18 +49,27 @@ const CustomMenu = (props: ICustomMenuDropdownProps) => {
|
|||
setIsOpen(true);
|
||||
if (referenceElement) referenceElement.focus();
|
||||
};
|
||||
const closeDropdown = () => setIsOpen(false);
|
||||
const handleKeyDown = useDropdownKeyDown(openDropdown, closeDropdown, isOpen);
|
||||
const closeDropdown = () => {
|
||||
isOpen && onMenuClose && onMenuClose();
|
||||
setIsOpen(false);
|
||||
};
|
||||
|
||||
const handleOnChange = () => {
|
||||
if (closeOnSelect) closeDropdown();
|
||||
};
|
||||
|
||||
const selectActiveItem = () => {
|
||||
const activeItem: HTMLElement | undefined | null = dropdownRef.current?.querySelector(
|
||||
`[data-headlessui-state="active"] button`
|
||||
);
|
||||
activeItem?.click();
|
||||
};
|
||||
|
||||
const handleKeyDown = useDropdownKeyDown(openDropdown, closeDropdown, isOpen, selectActiveItem);
|
||||
useOutsideClickDetector(dropdownRef, closeDropdown);
|
||||
|
||||
let menuItems = (
|
||||
<Menu.Items
|
||||
className="fixed z-10"
|
||||
onClick={() => {
|
||||
if (closeOnSelect) closeDropdown();
|
||||
}}
|
||||
static
|
||||
>
|
||||
<Menu.Items className="fixed z-10" static>
|
||||
<div
|
||||
className={cn(
|
||||
"my-1 overflow-y-scroll rounded-md border-[0.5px] border-custom-border-300 bg-custom-background-100 px-2 py-2.5 text-xs shadow-custom-shadow-rg focus:outline-none min-w-[12rem] whitespace-nowrap",
|
||||
|
|
@ -89,7 +100,8 @@ const CustomMenu = (props: ICustomMenuDropdownProps) => {
|
|||
ref={dropdownRef}
|
||||
tabIndex={tabIndex}
|
||||
className={cn("relative w-min text-left", className)}
|
||||
onKeyDown={handleKeyDown}
|
||||
onKeyDownCapture={handleKeyDown}
|
||||
onChange={handleOnChange}
|
||||
>
|
||||
{({ open }) => (
|
||||
<>
|
||||
|
|
@ -103,6 +115,7 @@ const CustomMenu = (props: ICustomMenuDropdownProps) => {
|
|||
if (menuButtonOnClick) menuButtonOnClick();
|
||||
}}
|
||||
className={customButtonClassName}
|
||||
tabIndex={customButtonTabIndex}
|
||||
>
|
||||
{customButton}
|
||||
</button>
|
||||
|
|
@ -122,6 +135,7 @@ const CustomMenu = (props: ICustomMenuDropdownProps) => {
|
|||
className={`relative grid place-items-center rounded p-1 text-custom-text-200 outline-none hover:text-custom-text-100 ${
|
||||
disabled ? "cursor-not-allowed" : "cursor-pointer hover:bg-custom-background-80"
|
||||
} ${buttonClassName}`}
|
||||
tabIndex={customButtonTabIndex}
|
||||
>
|
||||
<MoreHorizontal className={`h-3.5 w-3.5 ${verticalEllipsis ? "rotate-90" : ""}`} />
|
||||
</button>
|
||||
|
|
@ -142,6 +156,7 @@ const CustomMenu = (props: ICustomMenuDropdownProps) => {
|
|||
openDropdown();
|
||||
if (menuButtonOnClick) menuButtonOnClick();
|
||||
}}
|
||||
tabIndex={customButtonTabIndex}
|
||||
>
|
||||
{label}
|
||||
{!noChevron && <ChevronDown className="h-3.5 w-3.5" />}
|
||||
|
|
@ -159,6 +174,7 @@ const CustomMenu = (props: ICustomMenuDropdownProps) => {
|
|||
|
||||
const MenuItem: React.FC<ICustomMenuItemProps> = (props) => {
|
||||
const { children, onClick, className = "" } = props;
|
||||
|
||||
return (
|
||||
<Menu.Item as="div">
|
||||
{({ active, close }) => (
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import { Placement } from "@blueprintjs/popover2";
|
|||
|
||||
export interface IDropdownProps {
|
||||
customButtonClassName?: string;
|
||||
customButtonTabIndex?: number;
|
||||
buttonClassName?: string;
|
||||
className?: string;
|
||||
customButton?: JSX.Element;
|
||||
|
|
@ -23,6 +24,7 @@ export interface ICustomMenuDropdownProps extends IDropdownProps {
|
|||
noBorder?: boolean;
|
||||
verticalEllipsis?: boolean;
|
||||
menuButtonOnClick?: (...args: any) => void;
|
||||
onMenuClose?: () => void;
|
||||
closeOnSelect?: boolean;
|
||||
portalElement?: Element | null;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,16 +1,23 @@
|
|||
import { useCallback } from "react";
|
||||
|
||||
type TUseDropdownKeyDown = {
|
||||
(onOpen: () => void, onClose: () => void, isOpen: boolean): (event: React.KeyboardEvent<HTMLElement>) => void;
|
||||
(
|
||||
onOpen: () => void,
|
||||
onClose: () => void,
|
||||
isOpen: boolean,
|
||||
selectActiveItem?: () => void
|
||||
): (event: React.KeyboardEvent<HTMLElement>) => void;
|
||||
};
|
||||
|
||||
export const useDropdownKeyDown: TUseDropdownKeyDown = (onOpen, onClose, isOpen) => {
|
||||
export const useDropdownKeyDown: TUseDropdownKeyDown = (onOpen, onClose, isOpen, selectActiveItem?) => {
|
||||
const handleKeyDown = useCallback(
|
||||
(event: React.KeyboardEvent<HTMLElement>) => {
|
||||
if (event.key === "Enter") {
|
||||
event.stopPropagation();
|
||||
if (!isOpen) {
|
||||
event.stopPropagation();
|
||||
onOpen();
|
||||
} else {
|
||||
selectActiveItem && selectActiveItem();
|
||||
}
|
||||
} else if (event.key === "Escape" && isOpen) {
|
||||
event.stopPropagation();
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue