mirror of
https://github.com/makeplane/plane
synced 2024-06-14 14:31:34 +00:00
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
@ -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();
|
||||
|
@ -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
|
||||
|
@ -23,6 +23,7 @@ type Props = TDropdownProps & {
|
||||
dropdownArrow?: boolean;
|
||||
dropdownArrowClassName?: string;
|
||||
onChange: (val: string | null) => void;
|
||||
onClose?: () => void;
|
||||
projectId: string;
|
||||
value: string | null;
|
||||
};
|
||||
@ -47,6 +48,7 @@ export const CycleDropdown: React.FC<Props> = observer((props) => {
|
||||
dropdownArrowClassName = "",
|
||||
hideIcon = false,
|
||||
onChange,
|
||||
onClose,
|
||||
placeholder = "Cycle",
|
||||
placement,
|
||||
projectId,
|
||||
@ -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 = () => {
|
||||
@ -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}
|
||||
|
@ -23,6 +23,7 @@ type Props = TDropdownProps & {
|
||||
minDate?: Date;
|
||||
maxDate?: Date;
|
||||
onChange: (val: Date | null) => void;
|
||||
onClose?: () => void;
|
||||
value: Date | string | null;
|
||||
closeOnSelect?: boolean;
|
||||
};
|
||||
@ -42,6 +43,7 @@ export const DateDropdown: React.FC<Props> = (props) => {
|
||||
minDate,
|
||||
maxDate,
|
||||
onChange,
|
||||
onClose,
|
||||
placeholder = "Date",
|
||||
placement,
|
||||
showTooltip = false,
|
||||
@ -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 = () => {
|
||||
@ -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,
|
||||
|
@ -22,6 +22,7 @@ type Props = TDropdownProps & {
|
||||
dropdownArrow?: boolean;
|
||||
dropdownArrowClassName?: string;
|
||||
onChange: (val: number | null) => void;
|
||||
onClose?: () => void;
|
||||
projectId: string;
|
||||
value: number | null;
|
||||
};
|
||||
@ -46,6 +47,7 @@ export const EstimateDropdown: React.FC<Props> = observer((props) => {
|
||||
dropdownArrowClassName = "",
|
||||
hideIcon = false,
|
||||
onChange,
|
||||
onClose,
|
||||
placeholder = "Estimate",
|
||||
placement,
|
||||
projectId,
|
||||
@ -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 = () => {
|
||||
@ -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}
|
||||
@ -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,
|
||||
|
@ -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) => {
|
||||
@ -36,6 +37,7 @@ export const ProjectMemberDropdown: React.FC<Props> = observer((props) => {
|
||||
hideIcon = false,
|
||||
multiple,
|
||||
onChange,
|
||||
onClose,
|
||||
placeholder = "Members",
|
||||
placement,
|
||||
projectId,
|
||||
@ -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 = () => {
|
||||
@ -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}
|
||||
@ -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,
|
||||
|
1
web/components/dropdowns/member/types.d.ts
vendored
1
web/components/dropdowns/member/types.d.ts
vendored
@ -5,6 +5,7 @@ export type MemberDropdownProps = TDropdownProps & {
|
||||
dropdownArrow?: boolean;
|
||||
dropdownArrowClassName?: string;
|
||||
placeholder?: string;
|
||||
onClose?: () => void;
|
||||
} & (
|
||||
| {
|
||||
multiple: false;
|
||||
|
@ -32,6 +32,7 @@ export const WorkspaceMemberDropdown: React.FC<MemberDropdownProps> = observer((
|
||||
hideIcon = false,
|
||||
multiple,
|
||||
onChange,
|
||||
onClose,
|
||||
placeholder = "Members",
|
||||
placement,
|
||||
showTooltip = false,
|
||||
@ -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 = () => {
|
||||
@ -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}
|
||||
@ -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,
|
||||
|
@ -24,6 +24,7 @@ type Props = TDropdownProps & {
|
||||
dropdownArrowClassName?: string;
|
||||
projectId: string;
|
||||
showCount?: boolean;
|
||||
onClose?: () => void;
|
||||
} & (
|
||||
| {
|
||||
multiple: false;
|
||||
@ -151,6 +152,7 @@ export const ModuleDropdown: React.FC<Props> = observer((props) => {
|
||||
hideIcon = false,
|
||||
multiple,
|
||||
onChange,
|
||||
onClose,
|
||||
placeholder = "Module",
|
||||
placement,
|
||||
projectId,
|
||||
@ -226,8 +228,10 @@ export const ModuleDropdown: React.FC<Props> = observer((props) => {
|
||||
};
|
||||
|
||||
const handleClose = () => {
|
||||
if (isOpen) setIsOpen(false);
|
||||
if (!isOpen) return;
|
||||
setIsOpen(false);
|
||||
if (referenceElement) referenceElement.blur();
|
||||
onClose && onClose();
|
||||
};
|
||||
|
||||
const toggleDropdown = () => {
|
||||
@ -271,7 +275,7 @@ export const ModuleDropdown: 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}
|
||||
@ -281,7 +285,7 @@ export const ModuleDropdown: 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,
|
||||
|
@ -23,6 +23,7 @@ type Props = TDropdownProps & {
|
||||
dropdownArrowClassName?: string;
|
||||
highlightUrgent?: boolean;
|
||||
onChange: (val: TIssuePriorities) => void;
|
||||
onClose?: () => void;
|
||||
value: TIssuePriorities;
|
||||
};
|
||||
|
||||
@ -260,6 +261,7 @@ export const PriorityDropdown: React.FC<Props> = (props) => {
|
||||
hideIcon = false,
|
||||
highlightUrgent = true,
|
||||
onChange,
|
||||
onClose,
|
||||
placement,
|
||||
showTooltip = false,
|
||||
tabIndex,
|
||||
@ -308,8 +310,10 @@ export const PriorityDropdown: React.FC<Props> = (props) => {
|
||||
};
|
||||
|
||||
const handleClose = () => {
|
||||
if (isOpen) setIsOpen(false);
|
||||
if (!isOpen) return;
|
||||
setIsOpen(false);
|
||||
if (referenceElement) referenceElement.blur();
|
||||
onClose && onClose();
|
||||
};
|
||||
|
||||
const toggleDropdown = () => {
|
||||
@ -360,7 +364,7 @@ export const PriorityDropdown: React.FC<Props> = (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}
|
||||
@ -370,7 +374,7 @@ export const PriorityDropdown: 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,
|
||||
|
@ -22,6 +22,7 @@ type Props = TDropdownProps & {
|
||||
dropdownArrow?: boolean;
|
||||
dropdownArrowClassName?: string;
|
||||
onChange: (val: string) => void;
|
||||
onClose?: () => void;
|
||||
value: string | null;
|
||||
};
|
||||
|
||||
@ -37,6 +38,7 @@ export const ProjectDropdown: React.FC<Props> = observer((props) => {
|
||||
dropdownArrowClassName = "",
|
||||
hideIcon = false,
|
||||
onChange,
|
||||
onClose,
|
||||
placeholder = "Project",
|
||||
placement,
|
||||
showTooltip = false,
|
||||
@ -97,7 +99,9 @@ export const ProjectDropdown: React.FC<Props> = observer((props) => {
|
||||
};
|
||||
|
||||
const handleClose = () => {
|
||||
if (isOpen) setIsOpen(false);
|
||||
if (!isOpen) return;
|
||||
setIsOpen(false);
|
||||
onClose && onClose();
|
||||
if (referenceElement) referenceElement.blur();
|
||||
};
|
||||
|
||||
@ -137,7 +141,7 @@ export const ProjectDropdown: 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}
|
||||
@ -147,7 +151,7 @@ export const ProjectDropdown: 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,
|
||||
|
@ -23,6 +23,7 @@ type Props = TDropdownProps & {
|
||||
dropdownArrow?: boolean;
|
||||
dropdownArrowClassName?: string;
|
||||
onChange: (val: string) => void;
|
||||
onClose?: () => void;
|
||||
projectId: string;
|
||||
value: string;
|
||||
};
|
||||
@ -39,6 +40,7 @@ export const StateDropdown: React.FC<Props> = observer((props) => {
|
||||
dropdownArrowClassName = "",
|
||||
hideIcon = false,
|
||||
onChange,
|
||||
onClose,
|
||||
placement,
|
||||
projectId,
|
||||
showTooltip = false,
|
||||
@ -94,7 +96,9 @@ export const StateDropdown: React.FC<Props> = observer((props) => {
|
||||
};
|
||||
|
||||
const handleClose = () => {
|
||||
if (isOpen) setIsOpen(false);
|
||||
if (!isOpen) return;
|
||||
setIsOpen(false);
|
||||
onClose && onClose();
|
||||
if (referenceElement) referenceElement.blur();
|
||||
};
|
||||
|
||||
@ -134,7 +138,7 @@ export const StateDropdown: 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}
|
||||
@ -144,7 +148,7 @@ export const StateDropdown: 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,
|
||||
|
@ -4,6 +4,7 @@ import { usePopper } from "react-popper";
|
||||
import { Check, ChevronDown, Search, Tags } from "lucide-react";
|
||||
// hooks
|
||||
import { useApplication, useLabel } from "hooks/store";
|
||||
import { useDropdownKeyDown } from "hooks/use-dropdown-key-down";
|
||||
// components
|
||||
import { Combobox } from "@headlessui/react";
|
||||
import { Tooltip } from "@plane/ui";
|
||||
@ -25,6 +26,7 @@ export interface IIssuePropertyLabels {
|
||||
maxRender?: number;
|
||||
noLabelBorder?: boolean;
|
||||
placeholderText?: string;
|
||||
onClose?: () => void;
|
||||
}
|
||||
|
||||
export const IssuePropertyLabels: React.FC<IIssuePropertyLabels> = observer((props) => {
|
||||
@ -33,6 +35,7 @@ export const IssuePropertyLabels: React.FC<IIssuePropertyLabels> = observer((pro
|
||||
value,
|
||||
defaultOptions = [],
|
||||
onChange,
|
||||
onClose,
|
||||
disabled,
|
||||
hideDropdownArrow = false,
|
||||
className,
|
||||
@ -64,6 +67,12 @@ export const IssuePropertyLabels: React.FC<IIssuePropertyLabels> = observer((pro
|
||||
}
|
||||
};
|
||||
|
||||
const handleClose = () => {
|
||||
onClose && onClose();
|
||||
};
|
||||
|
||||
const handleKeyDown = useDropdownKeyDown(openDropDown, handleClose, false);
|
||||
|
||||
const { styles, attributes } = usePopper(referenceElement, popperElement, {
|
||||
placement: placement ?? "bottom-start",
|
||||
modifiers: [
|
||||
@ -171,13 +180,14 @@ export const IssuePropertyLabels: React.FC<IIssuePropertyLabels> = observer((pro
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
disabled={disabled}
|
||||
onKeyDownCapture={handleKeyDown}
|
||||
multiple
|
||||
>
|
||||
<Combobox.Button as={Fragment}>
|
||||
<button
|
||||
ref={setReferenceElement}
|
||||
type="button"
|
||||
className={`flex w-full items-center justify-between gap-1 text-xs ${
|
||||
className={`clickable flex w-full items-center justify-between gap-1 text-xs ${
|
||||
disabled
|
||||
? "cursor-not-allowed text-custom-text-200"
|
||||
: value.length <= maxRender
|
||||
@ -205,7 +215,7 @@ export const IssuePropertyLabels: React.FC<IIssuePropertyLabels> = observer((pro
|
||||
value={query}
|
||||
onChange={(e) => setQuery(e.target.value)}
|
||||
placeholder="Search"
|
||||
displayValue={(assigned: any) => assigned?.name}
|
||||
displayValue={(assigned: any) => assigned?.name || ""}
|
||||
/>
|
||||
</div>
|
||||
<div className={`mt-2 max-h-48 space-y-1 overflow-y-scroll`}>
|
||||
@ -216,10 +226,10 @@ export const IssuePropertyLabels: React.FC<IIssuePropertyLabels> = observer((pro
|
||||
<Combobox.Option
|
||||
key={option.value}
|
||||
value={option.value}
|
||||
className={({ selected }) =>
|
||||
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 }) => (
|
||||
|
@ -7,12 +7,13 @@ import { TIssue } from "@plane/types";
|
||||
|
||||
type Props = {
|
||||
issue: TIssue;
|
||||
onClose: () => void;
|
||||
onChange: (issue: TIssue, data: Partial<TIssue>, updates: any) => void;
|
||||
disabled: boolean;
|
||||
};
|
||||
|
||||
export const SpreadsheetAssigneeColumn: React.FC<Props> = observer((props: Props) => {
|
||||
const { issue, onChange, disabled } = props;
|
||||
const { issue, onChange, disabled, onClose } = props;
|
||||
|
||||
return (
|
||||
<div className="h-11 border-b-[0.5px] border-custom-border-200">
|
||||
@ -37,6 +38,7 @@ export const SpreadsheetAssigneeColumn: React.FC<Props> = observer((props: Props
|
||||
}
|
||||
buttonClassName="text-left"
|
||||
buttonContainerClassName="w-full"
|
||||
onClose={onClose}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
@ -9,12 +9,13 @@ import { TIssue } from "@plane/types";
|
||||
|
||||
type Props = {
|
||||
issue: TIssue;
|
||||
onClose: () => void;
|
||||
onChange: (issue: TIssue, data: Partial<TIssue>, updates: any) => void;
|
||||
disabled: boolean;
|
||||
};
|
||||
|
||||
export const SpreadsheetDueDateColumn: React.FC<Props> = observer((props: Props) => {
|
||||
const { issue, onChange, disabled } = props;
|
||||
const { issue, onChange, disabled, onClose } = props;
|
||||
|
||||
return (
|
||||
<div className="h-11 border-b-[0.5px] border-custom-border-200">
|
||||
@ -36,6 +37,7 @@ export const SpreadsheetDueDateColumn: React.FC<Props> = observer((props: Props)
|
||||
buttonVariant="transparent-with-text"
|
||||
buttonClassName="rounded-none text-left"
|
||||
buttonContainerClassName="w-full"
|
||||
onClose={onClose}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
@ -6,12 +6,13 @@ import { TIssue } from "@plane/types";
|
||||
|
||||
type Props = {
|
||||
issue: TIssue;
|
||||
onClose: () => void;
|
||||
onChange: (issue: TIssue, data: Partial<TIssue>, updates: any) => void;
|
||||
disabled: boolean;
|
||||
};
|
||||
|
||||
export const SpreadsheetEstimateColumn: React.FC<Props> = observer((props: Props) => {
|
||||
const { issue, onChange, disabled } = props;
|
||||
const { issue, onChange, disabled, onClose } = props;
|
||||
|
||||
return (
|
||||
<div className="h-11 border-b-[0.5px] border-custom-border-200">
|
||||
@ -25,6 +26,7 @@ export const SpreadsheetEstimateColumn: React.FC<Props> = observer((props: Props
|
||||
buttonVariant="transparent-with-text"
|
||||
buttonClassName="rounded-none text-left"
|
||||
buttonContainerClassName="w-full"
|
||||
onClose={onClose}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
@ -20,10 +20,11 @@ interface Props {
|
||||
property: keyof IIssueDisplayProperties;
|
||||
displayFilters: IIssueDisplayFilterOptions;
|
||||
handleDisplayFilterUpdate: (data: Partial<IIssueDisplayFilterOptions>) => 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 (
|
||||
<CustomMenu
|
||||
customButtonClassName="!w-full"
|
||||
customButtonClassName="clickable !w-full"
|
||||
customButtonTabIndex={-1}
|
||||
className="!w-full"
|
||||
customButton={
|
||||
<div className="flex w-full cursor-pointer items-center justify-between gap-1.5 py-2 text-sm text-custom-text-200 hover:text-custom-text-100">
|
||||
@ -62,6 +64,7 @@ export const SpreadsheetHeaderColumn = (props: Props) => {
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
onMenuClose={onClose}
|
||||
placement="bottom-end"
|
||||
>
|
||||
<CustomMenu.MenuItem onClick={() => handleOrderBy(propertyDetails.ascendingOrderKey, property)}>
|
||||
|
@ -9,12 +9,13 @@ import { TIssue } from "@plane/types";
|
||||
|
||||
type Props = {
|
||||
issue: TIssue;
|
||||
onClose: () => void;
|
||||
onChange: (issue: TIssue, data: Partial<TIssue>, updates: any) => void;
|
||||
disabled: boolean;
|
||||
};
|
||||
|
||||
export const SpreadsheetLabelColumn: React.FC<Props> = 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<Props> = 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}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
@ -7,22 +7,24 @@ import { TIssue } from "@plane/types";
|
||||
|
||||
type Props = {
|
||||
issue: TIssue;
|
||||
onClose: () => void;
|
||||
onChange: (issue: TIssue, data: Partial<TIssue>,updates:any) => void;
|
||||
disabled: boolean;
|
||||
};
|
||||
|
||||
export const SpreadsheetPriorityColumn: React.FC<Props> = observer((props: Props) => {
|
||||
const { issue, onChange, disabled } = props;
|
||||
const { issue, onChange, disabled, onClose } = props;
|
||||
|
||||
return (
|
||||
<div className="h-11 border-b-[0.5px] border-custom-border-200">
|
||||
<PriorityDropdown
|
||||
value={issue.priority}
|
||||
onChange={(data) => 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}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
@ -9,12 +9,13 @@ import { TIssue } from "@plane/types";
|
||||
|
||||
type Props = {
|
||||
issue: TIssue;
|
||||
onClose: () => void;
|
||||
onChange: (issue: TIssue, data: Partial<TIssue>, updates: any) => void;
|
||||
disabled: boolean;
|
||||
};
|
||||
|
||||
export const SpreadsheetStartDateColumn: React.FC<Props> = observer((props: Props) => {
|
||||
const { issue, onChange, disabled } = props;
|
||||
const { issue, onChange, disabled, onClose } = props;
|
||||
|
||||
return (
|
||||
<div className="h-11 border-b-[0.5px] border-custom-border-200">
|
||||
@ -36,6 +37,7 @@ export const SpreadsheetStartDateColumn: React.FC<Props> = observer((props: Prop
|
||||
buttonVariant="transparent-with-text"
|
||||
buttonClassName="rounded-none text-left"
|
||||
buttonContainerClassName="w-full"
|
||||
onClose={onClose}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
@ -7,12 +7,13 @@ import { TIssue } from "@plane/types";
|
||||
|
||||
type Props = {
|
||||
issue: TIssue;
|
||||
onClose: () => void;
|
||||
onChange: (issue: TIssue, data: Partial<TIssue>, updates: any) => void;
|
||||
disabled: boolean;
|
||||
};
|
||||
|
||||
export const SpreadsheetStateColumn: React.FC<Props> = observer((props) => {
|
||||
const { issue, onChange, disabled } = props;
|
||||
const { issue, onChange, disabled, onClose } = props;
|
||||
|
||||
return (
|
||||
<div className="h-11 border-b-[0.5px] border-custom-border-200">
|
||||
@ -24,6 +25,7 @@ export const SpreadsheetStateColumn: React.FC<Props> = observer((props) => {
|
||||
buttonVariant="transparent-with-text"
|
||||
buttonClassName="rounded-none text-left"
|
||||
buttonContainerClassName="w-full"
|
||||
onClose={onClose}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
@ -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<void>;
|
||||
isEstimateEnabled: boolean;
|
||||
};
|
||||
|
||||
export const IssueColumn = observer((props: Props) => {
|
||||
const { displayProperties, issueDetail, disableUserActions, property, handleIssues, isEstimateEnabled } = props;
|
||||
// router
|
||||
const router = useRouter();
|
||||
const tableCellRef = useRef<HTMLTableCellElement | null>(null);
|
||||
const { captureIssueEvent } = useEventTracker();
|
||||
|
||||
const shouldRenderProperty = property === "estimate" ? isEstimateEnabled : true;
|
||||
|
||||
const { Column } = SPREADSHEET_PROPERTY_DETAILS[property];
|
||||
|
||||
return (
|
||||
<WithDisplayPropertiesHOC
|
||||
displayProperties={displayProperties}
|
||||
displayPropertyKey={property}
|
||||
shouldRenderProperty={shouldRenderProperty}
|
||||
>
|
||||
<td
|
||||
tabIndex={0}
|
||||
className="h-11 w-full min-w-[8rem] bg-custom-background-100 text-sm after:absolute after:w-full after:bottom-[-1px] after:border after:border-custom-border-100 border-r-[1px] border-custom-border-100 focus:border-custom-primary-70"
|
||||
ref={tableCellRef}
|
||||
>
|
||||
<Column
|
||||
issue={issueDetail}
|
||||
onChange={(issue: TIssue, data: Partial<TIssue>, 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();
|
||||
}}
|
||||
/>
|
||||
</td>
|
||||
</WithDisplayPropertiesHOC>
|
||||
);
|
||||
});
|
@ -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<boolean>(false);
|
||||
@ -106,11 +106,12 @@ export const SpreadsheetIssueRow = observer((props: Props) => {
|
||||
{/* first column/ issue name and key column */}
|
||||
<td
|
||||
className={cn(
|
||||
"sticky group left-0 h-11 w-[28rem] flex items-center bg-custom-background-100 text-sm after:absolute border-r-[0.5px] border-custom-border-200",
|
||||
"sticky group left-0 h-11 w-[28rem] flex items-center bg-custom-background-100 text-sm after:absolute border-r-[0.5px] border-custom-border-200 focus:border-custom-primary-70",
|
||||
{
|
||||
"border-b-[0.5px]": peekIssue?.issueId !== issueDetail.id,
|
||||
}
|
||||
)}
|
||||
tabIndex={0}
|
||||
>
|
||||
<WithDisplayPropertiesHOC displayProperties={displayProperties} displayPropertyKey="key">
|
||||
<div
|
||||
@ -149,11 +150,14 @@ export const SpreadsheetIssueRow = observer((props: Props) => {
|
||||
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"
|
||||
>
|
||||
<div className="w-full overflow-hidden">
|
||||
<Tooltip tooltipHeading="Title" tooltipContent={issueDetail.name}>
|
||||
<div className="h-full w-full cursor-pointer truncate px-4 py-2.5 text-left text-[0.825rem] text-custom-text-100">
|
||||
<div
|
||||
className="h-full w-full cursor-pointer truncate px-4 py-2.5 text-left text-[0.825rem] text-custom-text-100"
|
||||
tabIndex={-1}
|
||||
>
|
||||
{issueDetail.name}
|
||||
</div>
|
||||
</Tooltip>
|
||||
@ -161,40 +165,16 @@ export const SpreadsheetIssueRow = observer((props: Props) => {
|
||||
</ControlLink>
|
||||
</td>
|
||||
{/* Rest of the columns */}
|
||||
{SPREADSHEET_PROPERTY_LIST.map((property) => {
|
||||
const { Column } = SPREADSHEET_PROPERTY_DETAILS[property];
|
||||
|
||||
const shouldRenderProperty = property === "estimate" ? isEstimateEnabled : true;
|
||||
|
||||
return (
|
||||
<WithDisplayPropertiesHOC
|
||||
displayProperties={displayProperties}
|
||||
displayPropertyKey={property}
|
||||
shouldRenderProperty={shouldRenderProperty}
|
||||
>
|
||||
<td className="h-11 w-full min-w-[8rem] bg-custom-background-100 text-sm after:absolute after:w-full after:bottom-[-1px] after:border after:border-custom-border-100 border-r-[1px] border-custom-border-100">
|
||||
<Column
|
||||
issue={issueDetail}
|
||||
onChange={(issue: TIssue, data: Partial<TIssue>, 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}
|
||||
/>
|
||||
</td>
|
||||
</WithDisplayPropertiesHOC>
|
||||
);
|
||||
})}
|
||||
{SPREADSHEET_PROPERTY_LIST.map((property) => (
|
||||
<IssueColumn
|
||||
displayProperties={displayProperties}
|
||||
issueDetail={issueDetail}
|
||||
disableUserActions={disableUserActions}
|
||||
property={property}
|
||||
handleIssues={handleIssues}
|
||||
isEstimateEnabled={isEstimateEnabled}
|
||||
/>
|
||||
))}
|
||||
</tr>
|
||||
|
||||
{isExpanded &&
|
||||
|
@ -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<IIssueDisplayFilterOptions>) => void;
|
||||
}
|
||||
export const SpreadsheetHeaderColumn = observer((props: Props) => {
|
||||
const { displayProperties, displayFilters, property, isEstimateEnabled, handleDisplayFilterUpdate } = props;
|
||||
|
||||
//hooks
|
||||
const tableHeaderCellRef = useRef<HTMLTableCellElement | null>(null);
|
||||
|
||||
const shouldRenderProperty = property === "estimate" ? isEstimateEnabled : true;
|
||||
|
||||
return (
|
||||
<WithDisplayPropertiesHOC
|
||||
displayProperties={displayProperties}
|
||||
displayPropertyKey={property}
|
||||
shouldRenderProperty={shouldRenderProperty}
|
||||
>
|
||||
<th
|
||||
className="h-11 w-full min-w-[8rem] items-center bg-custom-background-90 text-sm font-medium px-4 py-1 border border-b-0 border-t-0 border-custom-border-100 focus:border-custom-primary-70"
|
||||
ref={tableHeaderCellRef}
|
||||
tabIndex={0}
|
||||
>
|
||||
<HeaderColumn
|
||||
displayFilters={displayFilters}
|
||||
handleDisplayFilterUpdate={handleDisplayFilterUpdate}
|
||||
property={property}
|
||||
onClose={() => {
|
||||
tableHeaderCellRef?.current?.focus();
|
||||
}}
|
||||
/>
|
||||
</th>
|
||||
</WithDisplayPropertiesHOC>
|
||||
);
|
||||
});
|
@ -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 (
|
||||
<thead className="sticky top-0 left-0 z-[1] border-b-[0.5px] border-custom-border-100">
|
||||
<tr>
|
||||
<th className="sticky left-0 z-[1] h-11 w-[28rem] flex items-center bg-custom-background-90 text-sm font-medium before:absolute before:h-full before:right-0 before:border-[0.5px] before:border-custom-border-100">
|
||||
<th
|
||||
className="sticky left-0 z-[1] h-11 w-[28rem] flex items-center bg-custom-background-90 text-sm font-medium before:absolute before:h-full before:right-0 before:border-[0.5px] before:border-custom-border-100"
|
||||
tabIndex={-1}
|
||||
>
|
||||
<WithDisplayPropertiesHOC displayProperties={displayProperties} displayPropertyKey="key">
|
||||
<span className="flex h-full w-24 flex-shrink-0 items-center px-4 py-2.5">
|
||||
<span className="mr-1.5 text-custom-text-400">#</span>ID
|
||||
@ -34,25 +36,15 @@ export const SpreadsheetHeader = (props: Props) => {
|
||||
</span>
|
||||
</th>
|
||||
|
||||
{SPREADSHEET_PROPERTY_LIST.map((property) => {
|
||||
const shouldRenderProperty = property === "estimate" ? isEstimateEnabled : true;
|
||||
|
||||
return (
|
||||
<WithDisplayPropertiesHOC
|
||||
displayProperties={displayProperties}
|
||||
displayPropertyKey={property}
|
||||
shouldRenderProperty={shouldRenderProperty}
|
||||
>
|
||||
<th className="h-11 w-full min-w-[8rem] items-center bg-custom-background-90 text-sm font-medium px-4 py-1 border border-b-0 border-t-0 border-custom-border-100">
|
||||
<SpreadsheetHeaderColumn
|
||||
displayFilters={displayFilters}
|
||||
handleDisplayFilterUpdate={handleDisplayFilterUpdate}
|
||||
property={property}
|
||||
/>
|
||||
</th>
|
||||
</WithDisplayPropertiesHOC>
|
||||
);
|
||||
})}
|
||||
{SPREADSHEET_PROPERTY_LIST.map((property) => (
|
||||
<SpreadsheetHeaderColumn
|
||||
property={property}
|
||||
displayProperties={displayProperties}
|
||||
displayFilters={displayFilters}
|
||||
handleDisplayFilterUpdate={handleDisplayFilterUpdate}
|
||||
isEstimateEnabled={isEstimateEnabled}
|
||||
/>
|
||||
))}
|
||||
</tr>
|
||||
</thead>
|
||||
);
|
||||
|
@ -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 (
|
||||
<table className="overflow-y-auto">
|
||||
<table className="overflow-y-auto" onKeyDown={handleKeyBoardNavigation}>
|
||||
<SpreadsheetHeader
|
||||
displayProperties={displayProperties}
|
||||
displayFilters={displayFilters}
|
||||
|
@ -28,6 +28,7 @@ export const SPREADSHEET_PROPERTY_DETAILS: {
|
||||
icon: FC<ISvgIcons>;
|
||||
Column: React.FC<{
|
||||
issue: TIssue;
|
||||
onClose: () => void;
|
||||
onChange: (issue: TIssue, data: Partial<TIssue>, updates: any) => void;
|
||||
disabled: boolean;
|
||||
}>;
|
||||
|
@ -1,23 +1,31 @@
|
||||
import { useCallback } from "react";
|
||||
|
||||
type TUseDropdownKeyDown = {
|
||||
(onEnterKeyDown: () => void, onEscKeyDown: () => void): (event: React.KeyboardEvent<HTMLElement>) => void;
|
||||
(onEnterKeyDown: () => void, onEscKeyDown: () => void, stopPropagation?: boolean): (
|
||||
event: React.KeyboardEvent<HTMLElement>
|
||||
) => void;
|
||||
};
|
||||
|
||||
export const useDropdownKeyDown: TUseDropdownKeyDown = (onEnterKeyDown, onEscKeyDown) => {
|
||||
export const useDropdownKeyDown: TUseDropdownKeyDown = (onEnterKeyDown, onEscKeyDown, stopPropagation = true) => {
|
||||
const stopEventPropagation = (event: React.KeyboardEvent<HTMLElement>) => {
|
||||
if (stopPropagation) {
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
}
|
||||
};
|
||||
|
||||
const handleKeyDown = useCallback(
|
||||
(event: React.KeyboardEvent<HTMLElement>) => {
|
||||
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;
|
||||
|
56
web/hooks/use-table-keyboard-navigation.tsx
Normal file
56
web/hooks/use-table-keyboard-navigation.tsx
Normal file
@ -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<HTMLTableElement>) {
|
||||
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;
|
||||
};
|
Loading…
Reference in New Issue
Block a user