From fb3dd77b66bed774cb4bbf58bd9f7502c8faffeb Mon Sep 17 00:00:00 2001
From: rahulramesha <71900764+rahulramesha@users.noreply.github.com>
Date: Thu, 8 Feb 2024 11:49:00 +0530
Subject: [PATCH] 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
---
packages/ui/src/dropdowns/custom-menu.tsx | 36 +++++++---
packages/ui/src/dropdowns/helper.tsx | 2 +
.../ui/src/hooks/use-dropdown-key-down.tsx | 13 +++-
web/components/common/breadcrumb-link.tsx | 2 +-
web/components/dropdowns/cycle.tsx | 8 ++-
web/components/dropdowns/date.tsx | 8 ++-
web/components/dropdowns/estimate.tsx | 10 ++-
.../dropdowns/member/project-member.tsx | 10 ++-
web/components/dropdowns/member/types.d.ts | 1 +
.../dropdowns/member/workspace-member.tsx | 9 ++-
web/components/dropdowns/module.tsx | 10 ++-
web/components/dropdowns/priority.tsx | 10 ++-
web/components/dropdowns/project.tsx | 10 ++-
web/components/dropdowns/state.tsx | 10 ++-
.../issue-layouts/properties/labels.tsx | 20 ++++--
.../spreadsheet/columns/assignee-column.tsx | 4 +-
.../spreadsheet/columns/due-date-column.tsx | 4 +-
.../spreadsheet/columns/estimate-column.tsx | 4 +-
.../spreadsheet/columns/header-column.tsx | 9 ++-
.../spreadsheet/columns/label-column.tsx | 6 +-
.../spreadsheet/columns/priority-column.tsx | 6 +-
.../spreadsheet/columns/start-date-column.tsx | 4 +-
.../spreadsheet/columns/state-column.tsx | 4 +-
.../spreadsheet/issue-column.tsx | 68 +++++++++++++++++++
.../issue-layouts/spreadsheet/issue-row.tsx | 60 ++++++----------
.../spreadsheet/spreadsheet-header-column.tsx | 46 +++++++++++++
.../spreadsheet/spreadsheet-header.tsx | 36 ++++------
.../spreadsheet/spreadsheet-table.tsx | 5 +-
web/constants/spreadsheet.ts | 1 +
web/hooks/use-dropdown-key-down.tsx | 22 ++++--
web/hooks/use-table-keyboard-navigation.tsx | 56 +++++++++++++++
31 files changed, 368 insertions(+), 126 deletions(-)
create mode 100644 web/components/issues/issue-layouts/spreadsheet/issue-column.tsx
create mode 100644 web/components/issues/issue-layouts/spreadsheet/spreadsheet-header-column.tsx
create mode 100644 web/hooks/use-table-keyboard-navigation.tsx
diff --git a/packages/ui/src/dropdowns/custom-menu.tsx b/packages/ui/src/dropdowns/custom-menu.tsx
index 7ef99370f..c7cce2475 100644
--- a/packages/ui/src/dropdowns/custom-menu.tsx
+++ b/packages/ui/src/dropdowns/custom-menu.tsx
@@ -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 = (
-
{
- if (closeOnSelect) closeDropdown();
- }}
- static
- >
+
{
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}
@@ -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}
>
@@ -142,6 +156,7 @@ const CustomMenu = (props: ICustomMenuDropdownProps) => {
openDropdown();
if (menuButtonOnClick) menuButtonOnClick();
}}
+ tabIndex={customButtonTabIndex}
>
{label}
{!noChevron &&
}
@@ -159,6 +174,7 @@ const CustomMenu = (props: ICustomMenuDropdownProps) => {
const MenuItem: React.FC
= (props) => {
const { children, onClick, className = "" } = props;
+
return (
{({ active, close }) => (
diff --git a/packages/ui/src/dropdowns/helper.tsx b/packages/ui/src/dropdowns/helper.tsx
index 06f1c44c0..930f332b9 100644
--- a/packages/ui/src/dropdowns/helper.tsx
+++ b/packages/ui/src/dropdowns/helper.tsx
@@ -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;
}
diff --git a/packages/ui/src/hooks/use-dropdown-key-down.tsx b/packages/ui/src/hooks/use-dropdown-key-down.tsx
index 1bb861477..b93a4d551 100644
--- a/packages/ui/src/hooks/use-dropdown-key-down.tsx
+++ b/packages/ui/src/hooks/use-dropdown-key-down.tsx
@@ -1,16 +1,23 @@
import { useCallback } from "react";
type TUseDropdownKeyDown = {
- (onOpen: () => void, onClose: () => void, isOpen: boolean): (event: React.KeyboardEvent) => void;
+ (
+ onOpen: () => void,
+ onClose: () => void,
+ isOpen: boolean,
+ selectActiveItem?: () => void
+ ): (event: React.KeyboardEvent) => void;
};
-export const useDropdownKeyDown: TUseDropdownKeyDown = (onOpen, onClose, isOpen) => {
+export const useDropdownKeyDown: TUseDropdownKeyDown = (onOpen, onClose, isOpen, selectActiveItem?) => {
const handleKeyDown = useCallback(
(event: React.KeyboardEvent) => {
if (event.key === "Enter") {
- event.stopPropagation();
if (!isOpen) {
+ event.stopPropagation();
onOpen();
+ } else {
+ selectActiveItem && selectActiveItem();
}
} else if (event.key === "Escape" && isOpen) {
event.stopPropagation();
diff --git a/web/components/common/breadcrumb-link.tsx b/web/components/common/breadcrumb-link.tsx
index aebd7fc02..e5f1dbce6 100644
--- a/web/components/common/breadcrumb-link.tsx
+++ b/web/components/common/breadcrumb-link.tsx
@@ -11,7 +11,7 @@ export const BreadcrumbLink: React.FC = (props) => {
const { href, label, icon } = props;
return (
-
+
{href ? (
void;
+ onClose?: () => void;
projectId: string;
value: string | null;
};
@@ -47,6 +48,7 @@ export const CycleDropdown: React.FC
= observer((props) => {
dropdownArrowClassName = "",
hideIcon = false,
onChange,
+ onClose,
placeholder = "Cycle",
placement,
projectId,
@@ -123,8 +125,10 @@ export const CycleDropdown: React.FC = observer((props) => {
};
const handleClose = () => {
- if (isOpen) setIsOpen(false);
+ if (!isOpen) return;
+ setIsOpen(false);
if (referenceElement) referenceElement.blur();
+ onClose && onClose();
};
const toggleDropdown = () => {
@@ -163,7 +167,7 @@ export const CycleDropdown: React.FC = observer((props) => {
@@ -216,10 +226,10 @@ export const IssuePropertyLabels: React.FC
= observer((pro
+ className={({ active, selected }) =>
`flex cursor-pointer select-none items-center justify-between gap-2 truncate rounded px-1 py-1.5 hover:bg-custom-background-80 ${
- selected ? "text-custom-text-100" : "text-custom-text-200"
- }`
+ active ? "bg-custom-background-80" : ""
+ } ${selected ? "text-custom-text-100" : "text-custom-text-200"}`
}
>
{({ selected }) => (
diff --git a/web/components/issues/issue-layouts/spreadsheet/columns/assignee-column.tsx b/web/components/issues/issue-layouts/spreadsheet/columns/assignee-column.tsx
index e63a94b8c..b9450141b 100644
--- a/web/components/issues/issue-layouts/spreadsheet/columns/assignee-column.tsx
+++ b/web/components/issues/issue-layouts/spreadsheet/columns/assignee-column.tsx
@@ -7,12 +7,13 @@ import { TIssue } from "@plane/types";
type Props = {
issue: TIssue;
+ onClose: () => void;
onChange: (issue: TIssue, data: Partial, updates: any) => void;
disabled: boolean;
};
export const SpreadsheetAssigneeColumn: React.FC = observer((props: Props) => {
- const { issue, onChange, disabled } = props;
+ const { issue, onChange, disabled, onClose } = props;
return (
@@ -37,6 +38,7 @@ export const SpreadsheetAssigneeColumn: React.FC
= observer((props: Props
}
buttonClassName="text-left"
buttonContainerClassName="w-full"
+ onClose={onClose}
/>
);
diff --git a/web/components/issues/issue-layouts/spreadsheet/columns/due-date-column.tsx b/web/components/issues/issue-layouts/spreadsheet/columns/due-date-column.tsx
index 775275ca4..c5674cee9 100644
--- a/web/components/issues/issue-layouts/spreadsheet/columns/due-date-column.tsx
+++ b/web/components/issues/issue-layouts/spreadsheet/columns/due-date-column.tsx
@@ -9,12 +9,13 @@ import { TIssue } from "@plane/types";
type Props = {
issue: TIssue;
+ onClose: () => void;
onChange: (issue: TIssue, data: Partial, updates: any) => void;
disabled: boolean;
};
export const SpreadsheetDueDateColumn: React.FC = observer((props: Props) => {
- const { issue, onChange, disabled } = props;
+ const { issue, onChange, disabled, onClose } = props;
return (
@@ -36,6 +37,7 @@ export const SpreadsheetDueDateColumn: React.FC
= observer((props: Props)
buttonVariant="transparent-with-text"
buttonClassName="rounded-none text-left"
buttonContainerClassName="w-full"
+ onClose={onClose}
/>
);
diff --git a/web/components/issues/issue-layouts/spreadsheet/columns/estimate-column.tsx b/web/components/issues/issue-layouts/spreadsheet/columns/estimate-column.tsx
index 0c86b24c0..f7a472b49 100644
--- a/web/components/issues/issue-layouts/spreadsheet/columns/estimate-column.tsx
+++ b/web/components/issues/issue-layouts/spreadsheet/columns/estimate-column.tsx
@@ -6,12 +6,13 @@ import { TIssue } from "@plane/types";
type Props = {
issue: TIssue;
+ onClose: () => void;
onChange: (issue: TIssue, data: Partial, updates: any) => void;
disabled: boolean;
};
export const SpreadsheetEstimateColumn: React.FC = observer((props: Props) => {
- const { issue, onChange, disabled } = props;
+ const { issue, onChange, disabled, onClose } = props;
return (
@@ -25,6 +26,7 @@ export const SpreadsheetEstimateColumn: React.FC
= observer((props: Props
buttonVariant="transparent-with-text"
buttonClassName="rounded-none text-left"
buttonContainerClassName="w-full"
+ onClose={onClose}
/>
);
diff --git a/web/components/issues/issue-layouts/spreadsheet/columns/header-column.tsx b/web/components/issues/issue-layouts/spreadsheet/columns/header-column.tsx
index dc9f8c7c6..73478c6ac 100644
--- a/web/components/issues/issue-layouts/spreadsheet/columns/header-column.tsx
+++ b/web/components/issues/issue-layouts/spreadsheet/columns/header-column.tsx
@@ -20,10 +20,11 @@ interface Props {
property: keyof IIssueDisplayProperties;
displayFilters: IIssueDisplayFilterOptions;
handleDisplayFilterUpdate: (data: Partial) => void;
+ onClose: () => void;
}
-export const SpreadsheetHeaderColumn = (props: Props) => {
- const { displayFilters, handleDisplayFilterUpdate, property } = props;
+export const HeaderColumn = (props: Props) => {
+ const { displayFilters, handleDisplayFilterUpdate, property, onClose } = props;
const { storedValue: selectedMenuItem, setValue: setSelectedMenuItem } = useLocalStorage(
"spreadsheetViewSorting",
@@ -44,7 +45,8 @@ export const SpreadsheetHeaderColumn = (props: Props) => {
return (
@@ -62,6 +64,7 @@ export const SpreadsheetHeaderColumn = (props: Props) => {
}
+ onMenuClose={onClose}
placement="bottom-end"
>
handleOrderBy(propertyDetails.ascendingOrderKey, property)}>
diff --git a/web/components/issues/issue-layouts/spreadsheet/columns/label-column.tsx b/web/components/issues/issue-layouts/spreadsheet/columns/label-column.tsx
index 2812fb1ec..60e429c9f 100644
--- a/web/components/issues/issue-layouts/spreadsheet/columns/label-column.tsx
+++ b/web/components/issues/issue-layouts/spreadsheet/columns/label-column.tsx
@@ -9,12 +9,13 @@ import { TIssue } from "@plane/types";
type Props = {
issue: TIssue;
+ onClose: () => void;
onChange: (issue: TIssue, data: Partial, updates: any) => void;
disabled: boolean;
};
export const SpreadsheetLabelColumn: React.FC = observer((props: Props) => {
- const { issue, onChange, disabled } = props;
+ const { issue, onChange, disabled, onClose } = props;
// hooks
const { labelMap } = useLabel();
@@ -25,13 +26,14 @@ export const SpreadsheetLabelColumn: React.FC = observer((props: Props) =
projectId={issue.project_id ?? null}
value={issue.label_ids}
defaultOptions={defaultLabelOptions}
- onChange={(data) => onChange(issue, { label_ids: data },{ changed_property: "labels", change_details: data })}
+ onChange={(data) => onChange(issue, { label_ids: data }, { changed_property: "labels", change_details: data })}
className="h-11 w-full border-b-[0.5px] border-custom-border-200 hover:bg-custom-background-80"
buttonClassName="px-2.5 h-full"
hideDropdownArrow
maxRender={1}
disabled={disabled}
placeholderText="Select labels"
+ onClose={onClose}
/>
);
});
diff --git a/web/components/issues/issue-layouts/spreadsheet/columns/priority-column.tsx b/web/components/issues/issue-layouts/spreadsheet/columns/priority-column.tsx
index 1961b8717..b8801559c 100644
--- a/web/components/issues/issue-layouts/spreadsheet/columns/priority-column.tsx
+++ b/web/components/issues/issue-layouts/spreadsheet/columns/priority-column.tsx
@@ -7,22 +7,24 @@ import { TIssue } from "@plane/types";
type Props = {
issue: TIssue;
+ onClose: () => void;
onChange: (issue: TIssue, data: Partial,updates:any) => void;
disabled: boolean;
};
export const SpreadsheetPriorityColumn: React.FC = observer((props: Props) => {
- const { issue, onChange, disabled } = props;
+ const { issue, onChange, disabled, onClose } = props;
return (
onChange(issue, { priority: data },{changed_property:"priority",change_details:data})}
+ onChange={(data) => onChange(issue, { priority: data }, { changed_property: "priority", change_details: data })}
disabled={disabled}
buttonVariant="transparent-with-text"
buttonClassName="rounded-none text-left"
buttonContainerClassName="w-full"
+ onClose={onClose}
/>
);
diff --git a/web/components/issues/issue-layouts/spreadsheet/columns/start-date-column.tsx b/web/components/issues/issue-layouts/spreadsheet/columns/start-date-column.tsx
index 076464f27..fcbd817b6 100644
--- a/web/components/issues/issue-layouts/spreadsheet/columns/start-date-column.tsx
+++ b/web/components/issues/issue-layouts/spreadsheet/columns/start-date-column.tsx
@@ -9,12 +9,13 @@ import { TIssue } from "@plane/types";
type Props = {
issue: TIssue;
+ onClose: () => void;
onChange: (issue: TIssue, data: Partial, updates: any) => void;
disabled: boolean;
};
export const SpreadsheetStartDateColumn: React.FC = observer((props: Props) => {
- const { issue, onChange, disabled } = props;
+ const { issue, onChange, disabled, onClose } = props;
return (
@@ -36,6 +37,7 @@ export const SpreadsheetStartDateColumn: React.FC
= observer((props: Prop
buttonVariant="transparent-with-text"
buttonClassName="rounded-none text-left"
buttonContainerClassName="w-full"
+ onClose={onClose}
/>
);
diff --git a/web/components/issues/issue-layouts/spreadsheet/columns/state-column.tsx b/web/components/issues/issue-layouts/spreadsheet/columns/state-column.tsx
index 83a7c8d0f..1a029db12 100644
--- a/web/components/issues/issue-layouts/spreadsheet/columns/state-column.tsx
+++ b/web/components/issues/issue-layouts/spreadsheet/columns/state-column.tsx
@@ -7,12 +7,13 @@ import { TIssue } from "@plane/types";
type Props = {
issue: TIssue;
+ onClose: () => void;
onChange: (issue: TIssue, data: Partial, updates: any) => void;
disabled: boolean;
};
export const SpreadsheetStateColumn: React.FC = observer((props) => {
- const { issue, onChange, disabled } = props;
+ const { issue, onChange, disabled, onClose } = props;
return (
@@ -24,6 +25,7 @@ export const SpreadsheetStateColumn: React.FC
= observer((props) => {
buttonVariant="transparent-with-text"
buttonClassName="rounded-none text-left"
buttonContainerClassName="w-full"
+ onClose={onClose}
/>
);
diff --git a/web/components/issues/issue-layouts/spreadsheet/issue-column.tsx b/web/components/issues/issue-layouts/spreadsheet/issue-column.tsx
new file mode 100644
index 000000000..5d2e62fa5
--- /dev/null
+++ b/web/components/issues/issue-layouts/spreadsheet/issue-column.tsx
@@ -0,0 +1,68 @@
+import { useRef } from "react";
+import { useRouter } from "next/router";
+// types
+import { IIssueDisplayProperties, TIssue } from "@plane/types";
+import { EIssueActions } from "../types";
+// constants
+import { SPREADSHEET_PROPERTY_DETAILS } from "constants/spreadsheet";
+// components
+import { WithDisplayPropertiesHOC } from "../properties/with-display-properties-HOC";
+import { useEventTracker } from "hooks/store";
+import { observer } from "mobx-react";
+
+type Props = {
+ displayProperties: IIssueDisplayProperties;
+ issueDetail: TIssue;
+ disableUserActions: boolean;
+ property: keyof IIssueDisplayProperties;
+ handleIssues: (issue: TIssue, action: EIssueActions) => Promise;
+ isEstimateEnabled: boolean;
+};
+
+export const IssueColumn = observer((props: Props) => {
+ const { displayProperties, issueDetail, disableUserActions, property, handleIssues, isEstimateEnabled } = props;
+ // router
+ const router = useRouter();
+ const tableCellRef = useRef(null);
+ const { captureIssueEvent } = useEventTracker();
+
+ const shouldRenderProperty = property === "estimate" ? isEstimateEnabled : true;
+
+ const { Column } = SPREADSHEET_PROPERTY_DETAILS[property];
+
+ return (
+
+
+ , updates: any) =>
+ handleIssues({ ...issue, ...data }, EIssueActions.UPDATE).then(() => {
+ captureIssueEvent({
+ eventName: "Issue updated",
+ payload: {
+ ...issue,
+ ...data,
+ element: "Spreadsheet layout",
+ },
+ updates: updates,
+ path: router.asPath,
+ });
+ })
+ }
+ disabled={disableUserActions}
+ onClose={() => {
+ tableCellRef?.current?.focus();
+ }}
+ />
+ |
+
+ );
+});
diff --git a/web/components/issues/issue-layouts/spreadsheet/issue-row.tsx b/web/components/issues/issue-layouts/spreadsheet/issue-row.tsx
index 40ee85df7..2a97045fe 100644
--- a/web/components/issues/issue-layouts/spreadsheet/issue-row.tsx
+++ b/web/components/issues/issue-layouts/spreadsheet/issue-row.tsx
@@ -4,14 +4,15 @@ import { observer } from "mobx-react-lite";
// icons
import { ChevronRight, MoreHorizontal } from "lucide-react";
// constants
-import { SPREADSHEET_PROPERTY_DETAILS, SPREADSHEET_PROPERTY_LIST } from "constants/spreadsheet";
+import { SPREADSHEET_PROPERTY_LIST } from "constants/spreadsheet";
// components
import { WithDisplayPropertiesHOC } from "../properties/with-display-properties-HOC";
+import { IssueColumn } from "./issue-column";
// ui
import { ControlLink, Tooltip } from "@plane/ui";
// hooks
import useOutsideClickDetector from "hooks/use-outside-click-detector";
-import { useEventTracker, useIssueDetail, useProject } from "hooks/store";
+import { useIssueDetail, useProject } from "hooks/store";
// helper
import { cn } from "helpers/common.helper";
// types
@@ -51,7 +52,6 @@ export const SpreadsheetIssueRow = observer((props: Props) => {
//hooks
const { getProjectById } = useProject();
const { peekIssue, setPeekIssue } = useIssueDetail();
- const { captureIssueEvent } = useEventTracker();
// states
const [isMenuActive, setIsMenuActive] = useState(false);
const [isExpanded, setExpanded] = useState(false);
@@ -106,11 +106,12 @@ export const SpreadsheetIssueRow = observer((props: Props) => {
{/* first column/ issue name and key column */}
{
href={`/${workspaceSlug}/projects/${issueDetail.project_id}/issues/${issueId}`}
target="_blank"
onClick={() => handleIssuePeekOverview(issueDetail)}
- className="w-full line-clamp-1 cursor-pointer text-sm text-custom-text-100"
+ className="clickable w-full line-clamp-1 cursor-pointer text-sm text-custom-text-100"
>
-
+
{issueDetail.name}
@@ -161,40 +165,16 @@ export const SpreadsheetIssueRow = observer((props: Props) => {
|
{/* Rest of the columns */}
- {SPREADSHEET_PROPERTY_LIST.map((property) => {
- const { Column } = SPREADSHEET_PROPERTY_DETAILS[property];
-
- const shouldRenderProperty = property === "estimate" ? isEstimateEnabled : true;
-
- return (
-
-
- , updates: any) =>
- handleIssues({ ...issue, ...data }, EIssueActions.UPDATE).then(() => {
- captureIssueEvent({
- eventName: "Issue updated",
- payload: {
- ...issue,
- ...data,
- element: "Spreadsheet layout",
- },
- updates: updates,
- path: router.asPath,
- });
- })
- }
- disabled={disableUserActions}
- />
- |
-
- );
- })}
+ {SPREADSHEET_PROPERTY_LIST.map((property) => (
+
+ ))}
{isExpanded &&
diff --git a/web/components/issues/issue-layouts/spreadsheet/spreadsheet-header-column.tsx b/web/components/issues/issue-layouts/spreadsheet/spreadsheet-header-column.tsx
new file mode 100644
index 000000000..588c7be9e
--- /dev/null
+++ b/web/components/issues/issue-layouts/spreadsheet/spreadsheet-header-column.tsx
@@ -0,0 +1,46 @@
+import { useRef } from "react";
+//types
+import { IIssueDisplayFilterOptions, IIssueDisplayProperties } from "@plane/types";
+//components
+import { WithDisplayPropertiesHOC } from "../properties/with-display-properties-HOC";
+import { HeaderColumn } from "./columns/header-column";
+import { observer } from "mobx-react";
+
+interface Props {
+ displayProperties: IIssueDisplayProperties;
+ property: keyof IIssueDisplayProperties;
+ isEstimateEnabled: boolean;
+ displayFilters: IIssueDisplayFilterOptions;
+ handleDisplayFilterUpdate: (data: Partial) => void;
+}
+export const SpreadsheetHeaderColumn = observer((props: Props) => {
+ const { displayProperties, displayFilters, property, isEstimateEnabled, handleDisplayFilterUpdate } = props;
+
+ //hooks
+ const tableHeaderCellRef = useRef(null);
+
+ const shouldRenderProperty = property === "estimate" ? isEstimateEnabled : true;
+
+ return (
+
+
+ {
+ tableHeaderCellRef?.current?.focus();
+ }}
+ />
+ |
+
+ );
+});
diff --git a/web/components/issues/issue-layouts/spreadsheet/spreadsheet-header.tsx b/web/components/issues/issue-layouts/spreadsheet/spreadsheet-header.tsx
index 704c9f904..64d1ec0e1 100644
--- a/web/components/issues/issue-layouts/spreadsheet/spreadsheet-header.tsx
+++ b/web/components/issues/issue-layouts/spreadsheet/spreadsheet-header.tsx
@@ -6,8 +6,7 @@ import { IIssueDisplayFilterOptions, IIssueDisplayProperties } from "@plane/type
import { SPREADSHEET_PROPERTY_LIST } from "constants/spreadsheet";
// components
import { WithDisplayPropertiesHOC } from "../properties/with-display-properties-HOC";
-import { SpreadsheetHeaderColumn } from "./columns/header-column";
-
+import { SpreadsheetHeaderColumn } from "./spreadsheet-header-column";
interface Props {
displayProperties: IIssueDisplayProperties;
@@ -22,7 +21,10 @@ export const SpreadsheetHeader = (props: Props) => {
return (
-
+ |
#ID
@@ -34,25 +36,15 @@ export const SpreadsheetHeader = (props: Props) => {
|
- {SPREADSHEET_PROPERTY_LIST.map((property) => {
- const shouldRenderProperty = property === "estimate" ? isEstimateEnabled : true;
-
- return (
-
-
-
- |
-
- );
- })}
+ {SPREADSHEET_PROPERTY_LIST.map((property) => (
+
+ ))}
);
diff --git a/web/components/issues/issue-layouts/spreadsheet/spreadsheet-table.tsx b/web/components/issues/issue-layouts/spreadsheet/spreadsheet-table.tsx
index 369e6633c..e63b01dfb 100644
--- a/web/components/issues/issue-layouts/spreadsheet/spreadsheet-table.tsx
+++ b/web/components/issues/issue-layouts/spreadsheet/spreadsheet-table.tsx
@@ -5,6 +5,7 @@ import { EIssueActions } from "../types";
//components
import { SpreadsheetIssueRow } from "./issue-row";
import { SpreadsheetHeader } from "./spreadsheet-header";
+import { useTableKeyboardNavigation } from "hooks/use-table-keyboard-navigation";
type Props = {
displayProperties: IIssueDisplayProperties;
@@ -35,8 +36,10 @@ export const SpreadsheetTable = observer((props: Props) => {
canEditProperties,
} = props;
+ const handleKeyBoardNavigation = useTableKeyboardNavigation();
+
return (
-
+
;
Column: React.FC<{
issue: TIssue;
+ onClose: () => void;
onChange: (issue: TIssue, data: Partial, updates: any) => void;
disabled: boolean;
}>;
diff --git a/web/hooks/use-dropdown-key-down.tsx b/web/hooks/use-dropdown-key-down.tsx
index 99511b0fc..228e35575 100644
--- a/web/hooks/use-dropdown-key-down.tsx
+++ b/web/hooks/use-dropdown-key-down.tsx
@@ -1,23 +1,31 @@
import { useCallback } from "react";
type TUseDropdownKeyDown = {
- (onEnterKeyDown: () => void, onEscKeyDown: () => void): (event: React.KeyboardEvent) => void;
+ (onEnterKeyDown: () => void, onEscKeyDown: () => void, stopPropagation?: boolean): (
+ event: React.KeyboardEvent
+ ) => void;
};
-export const useDropdownKeyDown: TUseDropdownKeyDown = (onEnterKeyDown, onEscKeyDown) => {
+export const useDropdownKeyDown: TUseDropdownKeyDown = (onEnterKeyDown, onEscKeyDown, stopPropagation = true) => {
+ const stopEventPropagation = (event: React.KeyboardEvent) => {
+ if (stopPropagation) {
+ event.stopPropagation();
+ event.preventDefault();
+ }
+ };
+
const handleKeyDown = useCallback(
(event: React.KeyboardEvent) => {
if (event.key === "Enter") {
- event.stopPropagation();
- event.preventDefault();
+ stopEventPropagation(event);
+
onEnterKeyDown();
} else if (event.key === "Escape") {
- event.stopPropagation();
- event.preventDefault();
+ stopEventPropagation(event);
onEscKeyDown();
}
},
- [onEnterKeyDown, onEscKeyDown]
+ [onEnterKeyDown, onEscKeyDown, stopEventPropagation]
);
return handleKeyDown;
diff --git a/web/hooks/use-table-keyboard-navigation.tsx b/web/hooks/use-table-keyboard-navigation.tsx
new file mode 100644
index 000000000..0d1c26f3c
--- /dev/null
+++ b/web/hooks/use-table-keyboard-navigation.tsx
@@ -0,0 +1,56 @@
+export const useTableKeyboardNavigation = () => {
+ const getPreviousRow = (element: HTMLElement) => {
+ const previousRow = element.closest("tr")?.previousSibling;
+
+ if (previousRow) return previousRow;
+ //if previous row does not exist in the parent check the row with the header of the table
+ return element.closest("tbody")?.previousSibling?.childNodes?.[0];
+ };
+
+ const getNextRow = (element: HTMLElement) => {
+ const nextRow = element.closest("tr")?.nextSibling;
+
+ if (nextRow) return nextRow;
+ //if next row does not exist in the parent check the row with the body of the table
+ return element.closest("thead")?.nextSibling?.childNodes?.[0];
+ };
+
+ const handleKeyBoardNavigation = function (e: React.KeyboardEvent) {
+ const element = e.target as HTMLElement;
+
+ if (!(element?.tagName === "TD" || element?.tagName === "TH")) return;
+
+ let c: HTMLElement | null = null;
+ if (e.key == "ArrowRight") {
+ // Right Arrow
+ c = element.nextSibling as HTMLElement;
+ } else if (e.key == "ArrowLeft") {
+ // Left Arrow
+ c = element.previousSibling as HTMLElement;
+ } else if (e.key == "ArrowUp") {
+ // Up Arrow
+ const index = Array.prototype.indexOf.call(element?.parentNode?.childNodes || [], element);
+ const prevRow = getPreviousRow(element);
+
+ c = prevRow?.childNodes?.[index] as HTMLElement;
+ } else if (e.key == "ArrowDown") {
+ // Down Arrow
+ const index = Array.prototype.indexOf.call(element?.parentNode?.childNodes || [], element);
+ const nextRow = getNextRow(element);
+
+ c = nextRow?.childNodes[index] as HTMLElement;
+ } else if (e.key == "Enter" || e.key == "Space") {
+ e.preventDefault();
+ (element?.querySelector(".clickable") as HTMLElement)?.click();
+ return;
+ }
+
+ if (!c) return;
+
+ e.preventDefault();
+ c?.focus();
+ c?.scrollIntoView({ behavior: "smooth", block: "center", inline: "end" });
+ };
+
+ return handleKeyBoardNavigation;
+};