Skip to content

Commit

Permalink
feat: Keyboard navigation spreadsheet layout for issues (#3564)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
rahulramesha authored and aaryan610 committed Feb 8, 2024
1 parent 88747d0 commit 28ddff9
Show file tree
Hide file tree
Showing 31 changed files with 368 additions and 126 deletions.
36 changes: 26 additions & 10 deletions packages/ui/src/dropdowns/custom-menu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ const CustomMenu = (props: ICustomMenuDropdownProps) => {
const {
buttonClassName = "",
customButtonClassName = "",
customButtonTabIndex = 0,
placement,
children,
className = "",
Expand All @@ -29,6 +30,7 @@ const CustomMenu = (props: ICustomMenuDropdownProps) => {
verticalEllipsis = false,
portalElement,
menuButtonOnClick,
onMenuClose,
tabIndex,
closeOnSelect,
} = props;
Expand All @@ -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",
Expand Down Expand Up @@ -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 }) => (
<>
Expand All @@ -103,6 +115,7 @@ const CustomMenu = (props: ICustomMenuDropdownProps) => {
if (menuButtonOnClick) menuButtonOnClick();
}}
className={customButtonClassName}
tabIndex={customButtonTabIndex}
>
{customButton}
</button>
Expand All @@ -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>
Expand All @@ -142,6 +156,7 @@ const CustomMenu = (props: ICustomMenuDropdownProps) => {
openDropdown();
if (menuButtonOnClick) menuButtonOnClick();
}}
tabIndex={customButtonTabIndex}
>
{label}
{!noChevron && <ChevronDown className="h-3.5 w-3.5" />}
Expand All @@ -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 }) => (
Expand Down
2 changes: 2 additions & 0 deletions packages/ui/src/dropdowns/helper.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { Placement } from "@blueprintjs/popover2";

export interface IDropdownProps {
customButtonClassName?: string;
customButtonTabIndex?: number;
buttonClassName?: string;
className?: string;
customButton?: JSX.Element;
Expand All @@ -23,6 +24,7 @@ export interface ICustomMenuDropdownProps extends IDropdownProps {
noBorder?: boolean;
verticalEllipsis?: boolean;
menuButtonOnClick?: (...args: any) => void;
onMenuClose?: () => void;
closeOnSelect?: boolean;
portalElement?: Element | null;
}
Expand Down
13 changes: 10 additions & 3 deletions packages/ui/src/hooks/use-dropdown-key-down.tsx
Original file line number Diff line number Diff line change
@@ -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();
Expand Down
2 changes: 1 addition & 1 deletion web/components/common/breadcrumb-link.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ export const BreadcrumbLink: React.FC<Props> = (props) => {
const { href, label, icon } = props;
return (
<Tooltip tooltipContent={label} position="bottom">
<li className="flex items-center space-x-2">
<li className="flex items-center space-x-2" tabIndex={-1}>
<div className="flex flex-wrap items-center gap-2.5">
{href ? (
<Link
Expand Down
8 changes: 6 additions & 2 deletions web/components/dropdowns/cycle.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ type Props = TDropdownProps & {
dropdownArrow?: boolean;
dropdownArrowClassName?: string;
onChange: (val: string | null) => void;
onClose?: () => void;
projectId: string;
value: string | null;
};
Expand All @@ -47,6 +48,7 @@ export const CycleDropdown: React.FC<Props> = observer((props) => {
dropdownArrowClassName = "",
hideIcon = false,
onChange,
onClose,
placeholder = "Cycle",
placement,
projectId,
Expand Down Expand Up @@ -123,8 +125,10 @@ export const CycleDropdown: React.FC<Props> = observer((props) => {
};

const handleClose = () => {
if (isOpen) setIsOpen(false);
if (!isOpen) return;
setIsOpen(false);
if (referenceElement) referenceElement.blur();
onClose && onClose();
};

const toggleDropdown = () => {
Expand Down Expand Up @@ -163,7 +167,7 @@ export const CycleDropdown: React.FC<Props> = observer((props) => {
<button
ref={setReferenceElement}
type="button"
className={cn("block h-full w-full outline-none", buttonContainerClassName)}
className={cn("clickable block h-full w-full outline-none", buttonContainerClassName)}
onClick={handleOnClick}
>
{button}
Expand Down
8 changes: 6 additions & 2 deletions web/components/dropdowns/date.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ type Props = TDropdownProps & {
minDate?: Date;
maxDate?: Date;
onChange: (val: Date | null) => void;
onClose?: () => void;
value: Date | string | null;
closeOnSelect?: boolean;
};
Expand All @@ -42,6 +43,7 @@ export const DateDropdown: React.FC<Props> = (props) => {
minDate,
maxDate,
onChange,
onClose,
placeholder = "Date",
placement,
showTooltip = false,
Expand Down Expand Up @@ -74,8 +76,10 @@ export const DateDropdown: React.FC<Props> = (props) => {
};

const handleClose = () => {
if (isOpen) setIsOpen(false);
if (!isOpen) return;
setIsOpen(false);
if (referenceElement) referenceElement.blur();
onClose && onClose();
};

const toggleDropdown = () => {
Expand Down Expand Up @@ -112,7 +116,7 @@ export const DateDropdown: React.FC<Props> = (props) => {
ref={setReferenceElement}
type="button"
className={cn(
"block h-full max-w-full outline-none",
"clickable block h-full max-w-full outline-none",
{
"cursor-not-allowed text-custom-text-200": disabled,
"cursor-pointer": !disabled,
Expand Down
10 changes: 7 additions & 3 deletions web/components/dropdowns/estimate.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ type Props = TDropdownProps & {
dropdownArrow?: boolean;
dropdownArrowClassName?: string;
onChange: (val: number | null) => void;
onClose?: () => void;
projectId: string;
value: number | null;
};
Expand All @@ -46,6 +47,7 @@ export const EstimateDropdown: React.FC<Props> = observer((props) => {
dropdownArrowClassName = "",
hideIcon = false,
onChange,
onClose,
placeholder = "Estimate",
placement,
projectId,
Expand Down Expand Up @@ -112,8 +114,10 @@ export const EstimateDropdown: React.FC<Props> = observer((props) => {
};

const handleClose = () => {
if (isOpen) setIsOpen(false);
if (!isOpen) return;
setIsOpen(false);
if (referenceElement) referenceElement.blur();
onClose && onClose();
};

const toggleDropdown = () => {
Expand Down Expand Up @@ -152,7 +156,7 @@ export const EstimateDropdown: React.FC<Props> = observer((props) => {
<button
ref={setReferenceElement}
type="button"
className={cn("block h-full w-full outline-none", buttonContainerClassName)}
className={cn("clickable block h-full w-full outline-none", buttonContainerClassName)}
onClick={handleOnClick}
>
{button}
Expand All @@ -162,7 +166,7 @@ export const EstimateDropdown: React.FC<Props> = observer((props) => {
ref={setReferenceElement}
type="button"
className={cn(
"block h-full max-w-full outline-none",
"clickable block h-full max-w-full outline-none",
{
"cursor-not-allowed text-custom-text-200": disabled,
"cursor-pointer": !disabled,
Expand Down
10 changes: 7 additions & 3 deletions web/components/dropdowns/member/project-member.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import { BUTTON_VARIANTS_WITH_TEXT } from "../constants";

type Props = {
projectId: string;
onClose?: () => void;
} & MemberDropdownProps;

export const ProjectMemberDropdown: React.FC<Props> = observer((props) => {
Expand All @@ -36,6 +37,7 @@ export const ProjectMemberDropdown: React.FC<Props> = observer((props) => {
hideIcon = false,
multiple,
onChange,
onClose,
placeholder = "Members",
placement,
projectId,
Expand Down Expand Up @@ -105,8 +107,10 @@ export const ProjectMemberDropdown: React.FC<Props> = observer((props) => {
};

const handleClose = () => {
if (isOpen) setIsOpen(false);
if (!isOpen) return;
setIsOpen(false);
if (referenceElement) referenceElement.blur();
onClose && onClose();
};

const toggleDropdown = () => {
Expand Down Expand Up @@ -144,7 +148,7 @@ export const ProjectMemberDropdown: React.FC<Props> = observer((props) => {
<button
ref={setReferenceElement}
type="button"
className={cn("block h-full w-full outline-none", buttonContainerClassName)}
className={cn("clickable block h-full w-full outline-none", buttonContainerClassName)}
onClick={handleOnClick}
>
{button}
Expand All @@ -154,7 +158,7 @@ export const ProjectMemberDropdown: React.FC<Props> = observer((props) => {
ref={setReferenceElement}
type="button"
className={cn(
"block h-full max-w-full outline-none",
"clickable block h-full max-w-full outline-none",
{
"cursor-not-allowed text-custom-text-200": disabled,
"cursor-pointer": !disabled,
Expand Down
1 change: 1 addition & 0 deletions web/components/dropdowns/member/types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ export type MemberDropdownProps = TDropdownProps & {
dropdownArrow?: boolean;
dropdownArrowClassName?: string;
placeholder?: string;
onClose?: () => void;
} & (
| {
multiple: false;
Expand Down
9 changes: 6 additions & 3 deletions web/components/dropdowns/member/workspace-member.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ export const WorkspaceMemberDropdown: React.FC<MemberDropdownProps> = observer((
hideIcon = false,
multiple,
onChange,
onClose,
placeholder = "Members",
placement,
showTooltip = false,
Expand Down Expand Up @@ -95,8 +96,10 @@ export const WorkspaceMemberDropdown: React.FC<MemberDropdownProps> = observer((
};

const handleClose = () => {
if (isOpen) setIsOpen(false);
if (!isOpen) return;
setIsOpen(false);
if (referenceElement) referenceElement.blur();
onClose && onClose();
};

const toggleDropdown = () => {
Expand Down Expand Up @@ -134,7 +137,7 @@ export const WorkspaceMemberDropdown: React.FC<MemberDropdownProps> = observer((
<button
ref={setReferenceElement}
type="button"
className={cn("block h-full w-full outline-none", buttonContainerClassName)}
className={cn("clickable block h-full w-full outline-none", buttonContainerClassName)}
onClick={handleOnClick}
>
{button}
Expand All @@ -144,7 +147,7 @@ export const WorkspaceMemberDropdown: React.FC<MemberDropdownProps> = observer((
ref={setReferenceElement}
type="button"
className={cn(
"block h-full max-w-full outline-none",
"clickable block h-full max-w-full outline-none",
{
"cursor-not-allowed text-custom-text-200": disabled,
"cursor-pointer": !disabled,
Expand Down
Loading

0 comments on commit 28ddff9

Please sign in to comment.