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 {
|
const {
|
||||||
buttonClassName = "",
|
buttonClassName = "",
|
||||||
customButtonClassName = "",
|
customButtonClassName = "",
|
||||||
|
customButtonTabIndex = 0,
|
||||||
placement,
|
placement,
|
||||||
children,
|
children,
|
||||||
className = "",
|
className = "",
|
||||||
@ -29,6 +30,7 @@ const CustomMenu = (props: ICustomMenuDropdownProps) => {
|
|||||||
verticalEllipsis = false,
|
verticalEllipsis = false,
|
||||||
portalElement,
|
portalElement,
|
||||||
menuButtonOnClick,
|
menuButtonOnClick,
|
||||||
|
onMenuClose,
|
||||||
tabIndex,
|
tabIndex,
|
||||||
closeOnSelect,
|
closeOnSelect,
|
||||||
} = props;
|
} = props;
|
||||||
@ -47,18 +49,27 @@ const CustomMenu = (props: ICustomMenuDropdownProps) => {
|
|||||||
setIsOpen(true);
|
setIsOpen(true);
|
||||||
if (referenceElement) referenceElement.focus();
|
if (referenceElement) referenceElement.focus();
|
||||||
};
|
};
|
||||||
const closeDropdown = () => setIsOpen(false);
|
const closeDropdown = () => {
|
||||||
const handleKeyDown = useDropdownKeyDown(openDropdown, closeDropdown, isOpen);
|
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);
|
useOutsideClickDetector(dropdownRef, closeDropdown);
|
||||||
|
|
||||||
let menuItems = (
|
let menuItems = (
|
||||||
<Menu.Items
|
<Menu.Items className="fixed z-10" static>
|
||||||
className="fixed z-10"
|
|
||||||
onClick={() => {
|
|
||||||
if (closeOnSelect) closeDropdown();
|
|
||||||
}}
|
|
||||||
static
|
|
||||||
>
|
|
||||||
<div
|
<div
|
||||||
className={cn(
|
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",
|
"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}
|
ref={dropdownRef}
|
||||||
tabIndex={tabIndex}
|
tabIndex={tabIndex}
|
||||||
className={cn("relative w-min text-left", className)}
|
className={cn("relative w-min text-left", className)}
|
||||||
onKeyDown={handleKeyDown}
|
onKeyDownCapture={handleKeyDown}
|
||||||
|
onChange={handleOnChange}
|
||||||
>
|
>
|
||||||
{({ open }) => (
|
{({ open }) => (
|
||||||
<>
|
<>
|
||||||
@ -103,6 +115,7 @@ const CustomMenu = (props: ICustomMenuDropdownProps) => {
|
|||||||
if (menuButtonOnClick) menuButtonOnClick();
|
if (menuButtonOnClick) menuButtonOnClick();
|
||||||
}}
|
}}
|
||||||
className={customButtonClassName}
|
className={customButtonClassName}
|
||||||
|
tabIndex={customButtonTabIndex}
|
||||||
>
|
>
|
||||||
{customButton}
|
{customButton}
|
||||||
</button>
|
</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 ${
|
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"
|
disabled ? "cursor-not-allowed" : "cursor-pointer hover:bg-custom-background-80"
|
||||||
} ${buttonClassName}`}
|
} ${buttonClassName}`}
|
||||||
|
tabIndex={customButtonTabIndex}
|
||||||
>
|
>
|
||||||
<MoreHorizontal className={`h-3.5 w-3.5 ${verticalEllipsis ? "rotate-90" : ""}`} />
|
<MoreHorizontal className={`h-3.5 w-3.5 ${verticalEllipsis ? "rotate-90" : ""}`} />
|
||||||
</button>
|
</button>
|
||||||
@ -142,6 +156,7 @@ const CustomMenu = (props: ICustomMenuDropdownProps) => {
|
|||||||
openDropdown();
|
openDropdown();
|
||||||
if (menuButtonOnClick) menuButtonOnClick();
|
if (menuButtonOnClick) menuButtonOnClick();
|
||||||
}}
|
}}
|
||||||
|
tabIndex={customButtonTabIndex}
|
||||||
>
|
>
|
||||||
{label}
|
{label}
|
||||||
{!noChevron && <ChevronDown className="h-3.5 w-3.5" />}
|
{!noChevron && <ChevronDown className="h-3.5 w-3.5" />}
|
||||||
@ -159,6 +174,7 @@ const CustomMenu = (props: ICustomMenuDropdownProps) => {
|
|||||||
|
|
||||||
const MenuItem: React.FC<ICustomMenuItemProps> = (props) => {
|
const MenuItem: React.FC<ICustomMenuItemProps> = (props) => {
|
||||||
const { children, onClick, className = "" } = props;
|
const { children, onClick, className = "" } = props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Menu.Item as="div">
|
<Menu.Item as="div">
|
||||||
{({ active, close }) => (
|
{({ active, close }) => (
|
||||||
|
@ -3,6 +3,7 @@ import { Placement } from "@blueprintjs/popover2";
|
|||||||
|
|
||||||
export interface IDropdownProps {
|
export interface IDropdownProps {
|
||||||
customButtonClassName?: string;
|
customButtonClassName?: string;
|
||||||
|
customButtonTabIndex?: number;
|
||||||
buttonClassName?: string;
|
buttonClassName?: string;
|
||||||
className?: string;
|
className?: string;
|
||||||
customButton?: JSX.Element;
|
customButton?: JSX.Element;
|
||||||
@ -23,6 +24,7 @@ export interface ICustomMenuDropdownProps extends IDropdownProps {
|
|||||||
noBorder?: boolean;
|
noBorder?: boolean;
|
||||||
verticalEllipsis?: boolean;
|
verticalEllipsis?: boolean;
|
||||||
menuButtonOnClick?: (...args: any) => void;
|
menuButtonOnClick?: (...args: any) => void;
|
||||||
|
onMenuClose?: () => void;
|
||||||
closeOnSelect?: boolean;
|
closeOnSelect?: boolean;
|
||||||
portalElement?: Element | null;
|
portalElement?: Element | null;
|
||||||
}
|
}
|
||||||
|
@ -1,16 +1,23 @@
|
|||||||
import { useCallback } from "react";
|
import { useCallback } from "react";
|
||||||
|
|
||||||
type TUseDropdownKeyDown = {
|
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(
|
const handleKeyDown = useCallback(
|
||||||
(event: React.KeyboardEvent<HTMLElement>) => {
|
(event: React.KeyboardEvent<HTMLElement>) => {
|
||||||
if (event.key === "Enter") {
|
if (event.key === "Enter") {
|
||||||
event.stopPropagation();
|
|
||||||
if (!isOpen) {
|
if (!isOpen) {
|
||||||
|
event.stopPropagation();
|
||||||
onOpen();
|
onOpen();
|
||||||
|
} else {
|
||||||
|
selectActiveItem && selectActiveItem();
|
||||||
}
|
}
|
||||||
} else if (event.key === "Escape" && isOpen) {
|
} else if (event.key === "Escape" && isOpen) {
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
|
@ -11,7 +11,7 @@ export const BreadcrumbLink: React.FC<Props> = (props) => {
|
|||||||
const { href, label, icon } = props;
|
const { href, label, icon } = props;
|
||||||
return (
|
return (
|
||||||
<Tooltip tooltipContent={label} position="bottom">
|
<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">
|
<div className="flex flex-wrap items-center gap-2.5">
|
||||||
{href ? (
|
{href ? (
|
||||||
<Link
|
<Link
|
||||||
|
@ -23,6 +23,7 @@ type Props = TDropdownProps & {
|
|||||||
dropdownArrow?: boolean;
|
dropdownArrow?: boolean;
|
||||||
dropdownArrowClassName?: string;
|
dropdownArrowClassName?: string;
|
||||||
onChange: (val: string | null) => void;
|
onChange: (val: string | null) => void;
|
||||||
|
onClose?: () => void;
|
||||||
projectId: string;
|
projectId: string;
|
||||||
value: string | null;
|
value: string | null;
|
||||||
};
|
};
|
||||||
@ -47,6 +48,7 @@ export const CycleDropdown: React.FC<Props> = observer((props) => {
|
|||||||
dropdownArrowClassName = "",
|
dropdownArrowClassName = "",
|
||||||
hideIcon = false,
|
hideIcon = false,
|
||||||
onChange,
|
onChange,
|
||||||
|
onClose,
|
||||||
placeholder = "Cycle",
|
placeholder = "Cycle",
|
||||||
placement,
|
placement,
|
||||||
projectId,
|
projectId,
|
||||||
@ -123,8 +125,10 @@ export const CycleDropdown: React.FC<Props> = observer((props) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleClose = () => {
|
const handleClose = () => {
|
||||||
if (isOpen) setIsOpen(false);
|
if (!isOpen) return;
|
||||||
|
setIsOpen(false);
|
||||||
if (referenceElement) referenceElement.blur();
|
if (referenceElement) referenceElement.blur();
|
||||||
|
onClose && onClose();
|
||||||
};
|
};
|
||||||
|
|
||||||
const toggleDropdown = () => {
|
const toggleDropdown = () => {
|
||||||
@ -163,7 +167,7 @@ export const CycleDropdown: React.FC<Props> = observer((props) => {
|
|||||||
<button
|
<button
|
||||||
ref={setReferenceElement}
|
ref={setReferenceElement}
|
||||||
type="button"
|
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}
|
onClick={handleOnClick}
|
||||||
>
|
>
|
||||||
{button}
|
{button}
|
||||||
|
@ -23,6 +23,7 @@ type Props = TDropdownProps & {
|
|||||||
minDate?: Date;
|
minDate?: Date;
|
||||||
maxDate?: Date;
|
maxDate?: Date;
|
||||||
onChange: (val: Date | null) => void;
|
onChange: (val: Date | null) => void;
|
||||||
|
onClose?: () => void;
|
||||||
value: Date | string | null;
|
value: Date | string | null;
|
||||||
closeOnSelect?: boolean;
|
closeOnSelect?: boolean;
|
||||||
};
|
};
|
||||||
@ -42,6 +43,7 @@ export const DateDropdown: React.FC<Props> = (props) => {
|
|||||||
minDate,
|
minDate,
|
||||||
maxDate,
|
maxDate,
|
||||||
onChange,
|
onChange,
|
||||||
|
onClose,
|
||||||
placeholder = "Date",
|
placeholder = "Date",
|
||||||
placement,
|
placement,
|
||||||
showTooltip = false,
|
showTooltip = false,
|
||||||
@ -74,8 +76,10 @@ export const DateDropdown: React.FC<Props> = (props) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleClose = () => {
|
const handleClose = () => {
|
||||||
if (isOpen) setIsOpen(false);
|
if (!isOpen) return;
|
||||||
|
setIsOpen(false);
|
||||||
if (referenceElement) referenceElement.blur();
|
if (referenceElement) referenceElement.blur();
|
||||||
|
onClose && onClose();
|
||||||
};
|
};
|
||||||
|
|
||||||
const toggleDropdown = () => {
|
const toggleDropdown = () => {
|
||||||
@ -112,7 +116,7 @@ export const DateDropdown: React.FC<Props> = (props) => {
|
|||||||
ref={setReferenceElement}
|
ref={setReferenceElement}
|
||||||
type="button"
|
type="button"
|
||||||
className={cn(
|
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-not-allowed text-custom-text-200": disabled,
|
||||||
"cursor-pointer": !disabled,
|
"cursor-pointer": !disabled,
|
||||||
|
@ -22,6 +22,7 @@ type Props = TDropdownProps & {
|
|||||||
dropdownArrow?: boolean;
|
dropdownArrow?: boolean;
|
||||||
dropdownArrowClassName?: string;
|
dropdownArrowClassName?: string;
|
||||||
onChange: (val: number | null) => void;
|
onChange: (val: number | null) => void;
|
||||||
|
onClose?: () => void;
|
||||||
projectId: string;
|
projectId: string;
|
||||||
value: number | null;
|
value: number | null;
|
||||||
};
|
};
|
||||||
@ -46,6 +47,7 @@ export const EstimateDropdown: React.FC<Props> = observer((props) => {
|
|||||||
dropdownArrowClassName = "",
|
dropdownArrowClassName = "",
|
||||||
hideIcon = false,
|
hideIcon = false,
|
||||||
onChange,
|
onChange,
|
||||||
|
onClose,
|
||||||
placeholder = "Estimate",
|
placeholder = "Estimate",
|
||||||
placement,
|
placement,
|
||||||
projectId,
|
projectId,
|
||||||
@ -112,8 +114,10 @@ export const EstimateDropdown: React.FC<Props> = observer((props) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleClose = () => {
|
const handleClose = () => {
|
||||||
if (isOpen) setIsOpen(false);
|
if (!isOpen) return;
|
||||||
|
setIsOpen(false);
|
||||||
if (referenceElement) referenceElement.blur();
|
if (referenceElement) referenceElement.blur();
|
||||||
|
onClose && onClose();
|
||||||
};
|
};
|
||||||
|
|
||||||
const toggleDropdown = () => {
|
const toggleDropdown = () => {
|
||||||
@ -152,7 +156,7 @@ export const EstimateDropdown: React.FC<Props> = observer((props) => {
|
|||||||
<button
|
<button
|
||||||
ref={setReferenceElement}
|
ref={setReferenceElement}
|
||||||
type="button"
|
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}
|
onClick={handleOnClick}
|
||||||
>
|
>
|
||||||
{button}
|
{button}
|
||||||
@ -162,7 +166,7 @@ export const EstimateDropdown: React.FC<Props> = observer((props) => {
|
|||||||
ref={setReferenceElement}
|
ref={setReferenceElement}
|
||||||
type="button"
|
type="button"
|
||||||
className={cn(
|
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-not-allowed text-custom-text-200": disabled,
|
||||||
"cursor-pointer": !disabled,
|
"cursor-pointer": !disabled,
|
||||||
|
@ -21,6 +21,7 @@ import { BUTTON_VARIANTS_WITH_TEXT } from "../constants";
|
|||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
projectId: string;
|
projectId: string;
|
||||||
|
onClose?: () => void;
|
||||||
} & MemberDropdownProps;
|
} & MemberDropdownProps;
|
||||||
|
|
||||||
export const ProjectMemberDropdown: React.FC<Props> = observer((props) => {
|
export const ProjectMemberDropdown: React.FC<Props> = observer((props) => {
|
||||||
@ -36,6 +37,7 @@ export const ProjectMemberDropdown: React.FC<Props> = observer((props) => {
|
|||||||
hideIcon = false,
|
hideIcon = false,
|
||||||
multiple,
|
multiple,
|
||||||
onChange,
|
onChange,
|
||||||
|
onClose,
|
||||||
placeholder = "Members",
|
placeholder = "Members",
|
||||||
placement,
|
placement,
|
||||||
projectId,
|
projectId,
|
||||||
@ -105,8 +107,10 @@ export const ProjectMemberDropdown: React.FC<Props> = observer((props) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleClose = () => {
|
const handleClose = () => {
|
||||||
if (isOpen) setIsOpen(false);
|
if (!isOpen) return;
|
||||||
|
setIsOpen(false);
|
||||||
if (referenceElement) referenceElement.blur();
|
if (referenceElement) referenceElement.blur();
|
||||||
|
onClose && onClose();
|
||||||
};
|
};
|
||||||
|
|
||||||
const toggleDropdown = () => {
|
const toggleDropdown = () => {
|
||||||
@ -144,7 +148,7 @@ export const ProjectMemberDropdown: React.FC<Props> = observer((props) => {
|
|||||||
<button
|
<button
|
||||||
ref={setReferenceElement}
|
ref={setReferenceElement}
|
||||||
type="button"
|
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}
|
onClick={handleOnClick}
|
||||||
>
|
>
|
||||||
{button}
|
{button}
|
||||||
@ -154,7 +158,7 @@ export const ProjectMemberDropdown: React.FC<Props> = observer((props) => {
|
|||||||
ref={setReferenceElement}
|
ref={setReferenceElement}
|
||||||
type="button"
|
type="button"
|
||||||
className={cn(
|
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-not-allowed text-custom-text-200": disabled,
|
||||||
"cursor-pointer": !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;
|
dropdownArrow?: boolean;
|
||||||
dropdownArrowClassName?: string;
|
dropdownArrowClassName?: string;
|
||||||
placeholder?: string;
|
placeholder?: string;
|
||||||
|
onClose?: () => void;
|
||||||
} & (
|
} & (
|
||||||
| {
|
| {
|
||||||
multiple: false;
|
multiple: false;
|
||||||
|
@ -32,6 +32,7 @@ export const WorkspaceMemberDropdown: React.FC<MemberDropdownProps> = observer((
|
|||||||
hideIcon = false,
|
hideIcon = false,
|
||||||
multiple,
|
multiple,
|
||||||
onChange,
|
onChange,
|
||||||
|
onClose,
|
||||||
placeholder = "Members",
|
placeholder = "Members",
|
||||||
placement,
|
placement,
|
||||||
showTooltip = false,
|
showTooltip = false,
|
||||||
@ -95,8 +96,10 @@ export const WorkspaceMemberDropdown: React.FC<MemberDropdownProps> = observer((
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleClose = () => {
|
const handleClose = () => {
|
||||||
if (isOpen) setIsOpen(false);
|
if (!isOpen) return;
|
||||||
|
setIsOpen(false);
|
||||||
if (referenceElement) referenceElement.blur();
|
if (referenceElement) referenceElement.blur();
|
||||||
|
onClose && onClose();
|
||||||
};
|
};
|
||||||
|
|
||||||
const toggleDropdown = () => {
|
const toggleDropdown = () => {
|
||||||
@ -134,7 +137,7 @@ export const WorkspaceMemberDropdown: React.FC<MemberDropdownProps> = observer((
|
|||||||
<button
|
<button
|
||||||
ref={setReferenceElement}
|
ref={setReferenceElement}
|
||||||
type="button"
|
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}
|
onClick={handleOnClick}
|
||||||
>
|
>
|
||||||
{button}
|
{button}
|
||||||
@ -144,7 +147,7 @@ export const WorkspaceMemberDropdown: React.FC<MemberDropdownProps> = observer((
|
|||||||
ref={setReferenceElement}
|
ref={setReferenceElement}
|
||||||
type="button"
|
type="button"
|
||||||
className={cn(
|
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-not-allowed text-custom-text-200": disabled,
|
||||||
"cursor-pointer": !disabled,
|
"cursor-pointer": !disabled,
|
||||||
|
@ -24,6 +24,7 @@ type Props = TDropdownProps & {
|
|||||||
dropdownArrowClassName?: string;
|
dropdownArrowClassName?: string;
|
||||||
projectId: string;
|
projectId: string;
|
||||||
showCount?: boolean;
|
showCount?: boolean;
|
||||||
|
onClose?: () => void;
|
||||||
} & (
|
} & (
|
||||||
| {
|
| {
|
||||||
multiple: false;
|
multiple: false;
|
||||||
@ -151,6 +152,7 @@ export const ModuleDropdown: React.FC<Props> = observer((props) => {
|
|||||||
hideIcon = false,
|
hideIcon = false,
|
||||||
multiple,
|
multiple,
|
||||||
onChange,
|
onChange,
|
||||||
|
onClose,
|
||||||
placeholder = "Module",
|
placeholder = "Module",
|
||||||
placement,
|
placement,
|
||||||
projectId,
|
projectId,
|
||||||
@ -226,8 +228,10 @@ export const ModuleDropdown: React.FC<Props> = observer((props) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleClose = () => {
|
const handleClose = () => {
|
||||||
if (isOpen) setIsOpen(false);
|
if (!isOpen) return;
|
||||||
|
setIsOpen(false);
|
||||||
if (referenceElement) referenceElement.blur();
|
if (referenceElement) referenceElement.blur();
|
||||||
|
onClose && onClose();
|
||||||
};
|
};
|
||||||
|
|
||||||
const toggleDropdown = () => {
|
const toggleDropdown = () => {
|
||||||
@ -271,7 +275,7 @@ export const ModuleDropdown: React.FC<Props> = observer((props) => {
|
|||||||
<button
|
<button
|
||||||
ref={setReferenceElement}
|
ref={setReferenceElement}
|
||||||
type="button"
|
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}
|
onClick={handleOnClick}
|
||||||
>
|
>
|
||||||
{button}
|
{button}
|
||||||
@ -281,7 +285,7 @@ export const ModuleDropdown: React.FC<Props> = observer((props) => {
|
|||||||
ref={setReferenceElement}
|
ref={setReferenceElement}
|
||||||
type="button"
|
type="button"
|
||||||
className={cn(
|
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-not-allowed text-custom-text-200": disabled,
|
||||||
"cursor-pointer": !disabled,
|
"cursor-pointer": !disabled,
|
||||||
|
@ -23,6 +23,7 @@ type Props = TDropdownProps & {
|
|||||||
dropdownArrowClassName?: string;
|
dropdownArrowClassName?: string;
|
||||||
highlightUrgent?: boolean;
|
highlightUrgent?: boolean;
|
||||||
onChange: (val: TIssuePriorities) => void;
|
onChange: (val: TIssuePriorities) => void;
|
||||||
|
onClose?: () => void;
|
||||||
value: TIssuePriorities;
|
value: TIssuePriorities;
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -260,6 +261,7 @@ export const PriorityDropdown: React.FC<Props> = (props) => {
|
|||||||
hideIcon = false,
|
hideIcon = false,
|
||||||
highlightUrgent = true,
|
highlightUrgent = true,
|
||||||
onChange,
|
onChange,
|
||||||
|
onClose,
|
||||||
placement,
|
placement,
|
||||||
showTooltip = false,
|
showTooltip = false,
|
||||||
tabIndex,
|
tabIndex,
|
||||||
@ -308,8 +310,10 @@ export const PriorityDropdown: React.FC<Props> = (props) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleClose = () => {
|
const handleClose = () => {
|
||||||
if (isOpen) setIsOpen(false);
|
if (!isOpen) return;
|
||||||
|
setIsOpen(false);
|
||||||
if (referenceElement) referenceElement.blur();
|
if (referenceElement) referenceElement.blur();
|
||||||
|
onClose && onClose();
|
||||||
};
|
};
|
||||||
|
|
||||||
const toggleDropdown = () => {
|
const toggleDropdown = () => {
|
||||||
@ -360,7 +364,7 @@ export const PriorityDropdown: React.FC<Props> = (props) => {
|
|||||||
<button
|
<button
|
||||||
ref={setReferenceElement}
|
ref={setReferenceElement}
|
||||||
type="button"
|
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}
|
onClick={handleOnClick}
|
||||||
>
|
>
|
||||||
{button}
|
{button}
|
||||||
@ -370,7 +374,7 @@ export const PriorityDropdown: React.FC<Props> = (props) => {
|
|||||||
ref={setReferenceElement}
|
ref={setReferenceElement}
|
||||||
type="button"
|
type="button"
|
||||||
className={cn(
|
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-not-allowed text-custom-text-200": disabled,
|
||||||
"cursor-pointer": !disabled,
|
"cursor-pointer": !disabled,
|
||||||
|
@ -22,6 +22,7 @@ type Props = TDropdownProps & {
|
|||||||
dropdownArrow?: boolean;
|
dropdownArrow?: boolean;
|
||||||
dropdownArrowClassName?: string;
|
dropdownArrowClassName?: string;
|
||||||
onChange: (val: string) => void;
|
onChange: (val: string) => void;
|
||||||
|
onClose?: () => void;
|
||||||
value: string | null;
|
value: string | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -37,6 +38,7 @@ export const ProjectDropdown: React.FC<Props> = observer((props) => {
|
|||||||
dropdownArrowClassName = "",
|
dropdownArrowClassName = "",
|
||||||
hideIcon = false,
|
hideIcon = false,
|
||||||
onChange,
|
onChange,
|
||||||
|
onClose,
|
||||||
placeholder = "Project",
|
placeholder = "Project",
|
||||||
placement,
|
placement,
|
||||||
showTooltip = false,
|
showTooltip = false,
|
||||||
@ -97,7 +99,9 @@ export const ProjectDropdown: React.FC<Props> = observer((props) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleClose = () => {
|
const handleClose = () => {
|
||||||
if (isOpen) setIsOpen(false);
|
if (!isOpen) return;
|
||||||
|
setIsOpen(false);
|
||||||
|
onClose && onClose();
|
||||||
if (referenceElement) referenceElement.blur();
|
if (referenceElement) referenceElement.blur();
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -137,7 +141,7 @@ export const ProjectDropdown: React.FC<Props> = observer((props) => {
|
|||||||
<button
|
<button
|
||||||
ref={setReferenceElement}
|
ref={setReferenceElement}
|
||||||
type="button"
|
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}
|
onClick={handleOnClick}
|
||||||
>
|
>
|
||||||
{button}
|
{button}
|
||||||
@ -147,7 +151,7 @@ export const ProjectDropdown: React.FC<Props> = observer((props) => {
|
|||||||
ref={setReferenceElement}
|
ref={setReferenceElement}
|
||||||
type="button"
|
type="button"
|
||||||
className={cn(
|
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-not-allowed text-custom-text-200": disabled,
|
||||||
"cursor-pointer": !disabled,
|
"cursor-pointer": !disabled,
|
||||||
|
@ -23,6 +23,7 @@ type Props = TDropdownProps & {
|
|||||||
dropdownArrow?: boolean;
|
dropdownArrow?: boolean;
|
||||||
dropdownArrowClassName?: string;
|
dropdownArrowClassName?: string;
|
||||||
onChange: (val: string) => void;
|
onChange: (val: string) => void;
|
||||||
|
onClose?: () => void;
|
||||||
projectId: string;
|
projectId: string;
|
||||||
value: string;
|
value: string;
|
||||||
};
|
};
|
||||||
@ -39,6 +40,7 @@ export const StateDropdown: React.FC<Props> = observer((props) => {
|
|||||||
dropdownArrowClassName = "",
|
dropdownArrowClassName = "",
|
||||||
hideIcon = false,
|
hideIcon = false,
|
||||||
onChange,
|
onChange,
|
||||||
|
onClose,
|
||||||
placement,
|
placement,
|
||||||
projectId,
|
projectId,
|
||||||
showTooltip = false,
|
showTooltip = false,
|
||||||
@ -94,7 +96,9 @@ export const StateDropdown: React.FC<Props> = observer((props) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleClose = () => {
|
const handleClose = () => {
|
||||||
if (isOpen) setIsOpen(false);
|
if (!isOpen) return;
|
||||||
|
setIsOpen(false);
|
||||||
|
onClose && onClose();
|
||||||
if (referenceElement) referenceElement.blur();
|
if (referenceElement) referenceElement.blur();
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -134,7 +138,7 @@ export const StateDropdown: React.FC<Props> = observer((props) => {
|
|||||||
<button
|
<button
|
||||||
ref={setReferenceElement}
|
ref={setReferenceElement}
|
||||||
type="button"
|
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}
|
onClick={handleOnClick}
|
||||||
>
|
>
|
||||||
{button}
|
{button}
|
||||||
@ -144,7 +148,7 @@ export const StateDropdown: React.FC<Props> = observer((props) => {
|
|||||||
ref={setReferenceElement}
|
ref={setReferenceElement}
|
||||||
type="button"
|
type="button"
|
||||||
className={cn(
|
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-not-allowed text-custom-text-200": disabled,
|
||||||
"cursor-pointer": !disabled,
|
"cursor-pointer": !disabled,
|
||||||
|
@ -4,6 +4,7 @@ import { usePopper } from "react-popper";
|
|||||||
import { Check, ChevronDown, Search, Tags } from "lucide-react";
|
import { Check, ChevronDown, Search, Tags } from "lucide-react";
|
||||||
// hooks
|
// hooks
|
||||||
import { useApplication, useLabel } from "hooks/store";
|
import { useApplication, useLabel } from "hooks/store";
|
||||||
|
import { useDropdownKeyDown } from "hooks/use-dropdown-key-down";
|
||||||
// components
|
// components
|
||||||
import { Combobox } from "@headlessui/react";
|
import { Combobox } from "@headlessui/react";
|
||||||
import { Tooltip } from "@plane/ui";
|
import { Tooltip } from "@plane/ui";
|
||||||
@ -25,6 +26,7 @@ export interface IIssuePropertyLabels {
|
|||||||
maxRender?: number;
|
maxRender?: number;
|
||||||
noLabelBorder?: boolean;
|
noLabelBorder?: boolean;
|
||||||
placeholderText?: string;
|
placeholderText?: string;
|
||||||
|
onClose?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const IssuePropertyLabels: React.FC<IIssuePropertyLabels> = observer((props) => {
|
export const IssuePropertyLabels: React.FC<IIssuePropertyLabels> = observer((props) => {
|
||||||
@ -33,6 +35,7 @@ export const IssuePropertyLabels: React.FC<IIssuePropertyLabels> = observer((pro
|
|||||||
value,
|
value,
|
||||||
defaultOptions = [],
|
defaultOptions = [],
|
||||||
onChange,
|
onChange,
|
||||||
|
onClose,
|
||||||
disabled,
|
disabled,
|
||||||
hideDropdownArrow = false,
|
hideDropdownArrow = false,
|
||||||
className,
|
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, {
|
const { styles, attributes } = usePopper(referenceElement, popperElement, {
|
||||||
placement: placement ?? "bottom-start",
|
placement: placement ?? "bottom-start",
|
||||||
modifiers: [
|
modifiers: [
|
||||||
@ -171,13 +180,14 @@ export const IssuePropertyLabels: React.FC<IIssuePropertyLabels> = observer((pro
|
|||||||
value={value}
|
value={value}
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
|
onKeyDownCapture={handleKeyDown}
|
||||||
multiple
|
multiple
|
||||||
>
|
>
|
||||||
<Combobox.Button as={Fragment}>
|
<Combobox.Button as={Fragment}>
|
||||||
<button
|
<button
|
||||||
ref={setReferenceElement}
|
ref={setReferenceElement}
|
||||||
type="button"
|
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
|
disabled
|
||||||
? "cursor-not-allowed text-custom-text-200"
|
? "cursor-not-allowed text-custom-text-200"
|
||||||
: value.length <= maxRender
|
: value.length <= maxRender
|
||||||
@ -205,7 +215,7 @@ export const IssuePropertyLabels: React.FC<IIssuePropertyLabels> = observer((pro
|
|||||||
value={query}
|
value={query}
|
||||||
onChange={(e) => setQuery(e.target.value)}
|
onChange={(e) => setQuery(e.target.value)}
|
||||||
placeholder="Search"
|
placeholder="Search"
|
||||||
displayValue={(assigned: any) => assigned?.name}
|
displayValue={(assigned: any) => assigned?.name || ""}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className={`mt-2 max-h-48 space-y-1 overflow-y-scroll`}>
|
<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
|
<Combobox.Option
|
||||||
key={option.value}
|
key={option.value}
|
||||||
value={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 ${
|
`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 }) => (
|
{({ selected }) => (
|
||||||
|
@ -7,12 +7,13 @@ import { TIssue } from "@plane/types";
|
|||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
issue: TIssue;
|
issue: TIssue;
|
||||||
|
onClose: () => void;
|
||||||
onChange: (issue: TIssue, data: Partial<TIssue>, updates: any) => void;
|
onChange: (issue: TIssue, data: Partial<TIssue>, updates: any) => void;
|
||||||
disabled: boolean;
|
disabled: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const SpreadsheetAssigneeColumn: React.FC<Props> = observer((props: Props) => {
|
export const SpreadsheetAssigneeColumn: React.FC<Props> = observer((props: Props) => {
|
||||||
const { issue, onChange, disabled } = props;
|
const { issue, onChange, disabled, onClose } = props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="h-11 border-b-[0.5px] border-custom-border-200">
|
<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"
|
buttonClassName="text-left"
|
||||||
buttonContainerClassName="w-full"
|
buttonContainerClassName="w-full"
|
||||||
|
onClose={onClose}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -9,12 +9,13 @@ import { TIssue } from "@plane/types";
|
|||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
issue: TIssue;
|
issue: TIssue;
|
||||||
|
onClose: () => void;
|
||||||
onChange: (issue: TIssue, data: Partial<TIssue>, updates: any) => void;
|
onChange: (issue: TIssue, data: Partial<TIssue>, updates: any) => void;
|
||||||
disabled: boolean;
|
disabled: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const SpreadsheetDueDateColumn: React.FC<Props> = observer((props: Props) => {
|
export const SpreadsheetDueDateColumn: React.FC<Props> = observer((props: Props) => {
|
||||||
const { issue, onChange, disabled } = props;
|
const { issue, onChange, disabled, onClose } = props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="h-11 border-b-[0.5px] border-custom-border-200">
|
<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"
|
buttonVariant="transparent-with-text"
|
||||||
buttonClassName="rounded-none text-left"
|
buttonClassName="rounded-none text-left"
|
||||||
buttonContainerClassName="w-full"
|
buttonContainerClassName="w-full"
|
||||||
|
onClose={onClose}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -6,12 +6,13 @@ import { TIssue } from "@plane/types";
|
|||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
issue: TIssue;
|
issue: TIssue;
|
||||||
|
onClose: () => void;
|
||||||
onChange: (issue: TIssue, data: Partial<TIssue>, updates: any) => void;
|
onChange: (issue: TIssue, data: Partial<TIssue>, updates: any) => void;
|
||||||
disabled: boolean;
|
disabled: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const SpreadsheetEstimateColumn: React.FC<Props> = observer((props: Props) => {
|
export const SpreadsheetEstimateColumn: React.FC<Props> = observer((props: Props) => {
|
||||||
const { issue, onChange, disabled } = props;
|
const { issue, onChange, disabled, onClose } = props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="h-11 border-b-[0.5px] border-custom-border-200">
|
<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"
|
buttonVariant="transparent-with-text"
|
||||||
buttonClassName="rounded-none text-left"
|
buttonClassName="rounded-none text-left"
|
||||||
buttonContainerClassName="w-full"
|
buttonContainerClassName="w-full"
|
||||||
|
onClose={onClose}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -20,10 +20,11 @@ interface Props {
|
|||||||
property: keyof IIssueDisplayProperties;
|
property: keyof IIssueDisplayProperties;
|
||||||
displayFilters: IIssueDisplayFilterOptions;
|
displayFilters: IIssueDisplayFilterOptions;
|
||||||
handleDisplayFilterUpdate: (data: Partial<IIssueDisplayFilterOptions>) => void;
|
handleDisplayFilterUpdate: (data: Partial<IIssueDisplayFilterOptions>) => void;
|
||||||
|
onClose: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const SpreadsheetHeaderColumn = (props: Props) => {
|
export const HeaderColumn = (props: Props) => {
|
||||||
const { displayFilters, handleDisplayFilterUpdate, property } = props;
|
const { displayFilters, handleDisplayFilterUpdate, property, onClose } = props;
|
||||||
|
|
||||||
const { storedValue: selectedMenuItem, setValue: setSelectedMenuItem } = useLocalStorage(
|
const { storedValue: selectedMenuItem, setValue: setSelectedMenuItem } = useLocalStorage(
|
||||||
"spreadsheetViewSorting",
|
"spreadsheetViewSorting",
|
||||||
@ -44,7 +45,8 @@ export const SpreadsheetHeaderColumn = (props: Props) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<CustomMenu
|
<CustomMenu
|
||||||
customButtonClassName="!w-full"
|
customButtonClassName="clickable !w-full"
|
||||||
|
customButtonTabIndex={-1}
|
||||||
className="!w-full"
|
className="!w-full"
|
||||||
customButton={
|
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">
|
<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>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
onMenuClose={onClose}
|
||||||
placement="bottom-end"
|
placement="bottom-end"
|
||||||
>
|
>
|
||||||
<CustomMenu.MenuItem onClick={() => handleOrderBy(propertyDetails.ascendingOrderKey, property)}>
|
<CustomMenu.MenuItem onClick={() => handleOrderBy(propertyDetails.ascendingOrderKey, property)}>
|
||||||
|
@ -9,12 +9,13 @@ import { TIssue } from "@plane/types";
|
|||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
issue: TIssue;
|
issue: TIssue;
|
||||||
|
onClose: () => void;
|
||||||
onChange: (issue: TIssue, data: Partial<TIssue>, updates: any) => void;
|
onChange: (issue: TIssue, data: Partial<TIssue>, updates: any) => void;
|
||||||
disabled: boolean;
|
disabled: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const SpreadsheetLabelColumn: React.FC<Props> = observer((props: Props) => {
|
export const SpreadsheetLabelColumn: React.FC<Props> = observer((props: Props) => {
|
||||||
const { issue, onChange, disabled } = props;
|
const { issue, onChange, disabled, onClose } = props;
|
||||||
// hooks
|
// hooks
|
||||||
const { labelMap } = useLabel();
|
const { labelMap } = useLabel();
|
||||||
|
|
||||||
@ -32,6 +33,7 @@ export const SpreadsheetLabelColumn: React.FC<Props> = observer((props: Props) =
|
|||||||
maxRender={1}
|
maxRender={1}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
placeholderText="Select labels"
|
placeholderText="Select labels"
|
||||||
|
onClose={onClose}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
@ -7,12 +7,13 @@ import { TIssue } from "@plane/types";
|
|||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
issue: TIssue;
|
issue: TIssue;
|
||||||
|
onClose: () => void;
|
||||||
onChange: (issue: TIssue, data: Partial<TIssue>,updates:any) => void;
|
onChange: (issue: TIssue, data: Partial<TIssue>,updates:any) => void;
|
||||||
disabled: boolean;
|
disabled: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const SpreadsheetPriorityColumn: React.FC<Props> = observer((props: Props) => {
|
export const SpreadsheetPriorityColumn: React.FC<Props> = observer((props: Props) => {
|
||||||
const { issue, onChange, disabled } = props;
|
const { issue, onChange, disabled, onClose } = props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="h-11 border-b-[0.5px] border-custom-border-200">
|
<div className="h-11 border-b-[0.5px] border-custom-border-200">
|
||||||
@ -23,6 +24,7 @@ export const SpreadsheetPriorityColumn: React.FC<Props> = observer((props: Props
|
|||||||
buttonVariant="transparent-with-text"
|
buttonVariant="transparent-with-text"
|
||||||
buttonClassName="rounded-none text-left"
|
buttonClassName="rounded-none text-left"
|
||||||
buttonContainerClassName="w-full"
|
buttonContainerClassName="w-full"
|
||||||
|
onClose={onClose}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -9,12 +9,13 @@ import { TIssue } from "@plane/types";
|
|||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
issue: TIssue;
|
issue: TIssue;
|
||||||
|
onClose: () => void;
|
||||||
onChange: (issue: TIssue, data: Partial<TIssue>, updates: any) => void;
|
onChange: (issue: TIssue, data: Partial<TIssue>, updates: any) => void;
|
||||||
disabled: boolean;
|
disabled: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const SpreadsheetStartDateColumn: React.FC<Props> = observer((props: Props) => {
|
export const SpreadsheetStartDateColumn: React.FC<Props> = observer((props: Props) => {
|
||||||
const { issue, onChange, disabled } = props;
|
const { issue, onChange, disabled, onClose } = props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="h-11 border-b-[0.5px] border-custom-border-200">
|
<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"
|
buttonVariant="transparent-with-text"
|
||||||
buttonClassName="rounded-none text-left"
|
buttonClassName="rounded-none text-left"
|
||||||
buttonContainerClassName="w-full"
|
buttonContainerClassName="w-full"
|
||||||
|
onClose={onClose}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -7,12 +7,13 @@ import { TIssue } from "@plane/types";
|
|||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
issue: TIssue;
|
issue: TIssue;
|
||||||
|
onClose: () => void;
|
||||||
onChange: (issue: TIssue, data: Partial<TIssue>, updates: any) => void;
|
onChange: (issue: TIssue, data: Partial<TIssue>, updates: any) => void;
|
||||||
disabled: boolean;
|
disabled: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const SpreadsheetStateColumn: React.FC<Props> = observer((props) => {
|
export const SpreadsheetStateColumn: React.FC<Props> = observer((props) => {
|
||||||
const { issue, onChange, disabled } = props;
|
const { issue, onChange, disabled, onClose } = props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="h-11 border-b-[0.5px] border-custom-border-200">
|
<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"
|
buttonVariant="transparent-with-text"
|
||||||
buttonClassName="rounded-none text-left"
|
buttonClassName="rounded-none text-left"
|
||||||
buttonContainerClassName="w-full"
|
buttonContainerClassName="w-full"
|
||||||
|
onClose={onClose}
|
||||||
/>
|
/>
|
||||||
</div>
|
</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
|
// icons
|
||||||
import { ChevronRight, MoreHorizontal } from "lucide-react";
|
import { ChevronRight, MoreHorizontal } from "lucide-react";
|
||||||
// constants
|
// constants
|
||||||
import { SPREADSHEET_PROPERTY_DETAILS, SPREADSHEET_PROPERTY_LIST } from "constants/spreadsheet";
|
import { SPREADSHEET_PROPERTY_LIST } from "constants/spreadsheet";
|
||||||
// components
|
// components
|
||||||
import { WithDisplayPropertiesHOC } from "../properties/with-display-properties-HOC";
|
import { WithDisplayPropertiesHOC } from "../properties/with-display-properties-HOC";
|
||||||
|
import { IssueColumn } from "./issue-column";
|
||||||
// ui
|
// ui
|
||||||
import { ControlLink, Tooltip } from "@plane/ui";
|
import { ControlLink, Tooltip } from "@plane/ui";
|
||||||
// hooks
|
// hooks
|
||||||
import useOutsideClickDetector from "hooks/use-outside-click-detector";
|
import useOutsideClickDetector from "hooks/use-outside-click-detector";
|
||||||
import { useEventTracker, useIssueDetail, useProject } from "hooks/store";
|
import { useIssueDetail, useProject } from "hooks/store";
|
||||||
// helper
|
// helper
|
||||||
import { cn } from "helpers/common.helper";
|
import { cn } from "helpers/common.helper";
|
||||||
// types
|
// types
|
||||||
@ -51,7 +52,6 @@ export const SpreadsheetIssueRow = observer((props: Props) => {
|
|||||||
//hooks
|
//hooks
|
||||||
const { getProjectById } = useProject();
|
const { getProjectById } = useProject();
|
||||||
const { peekIssue, setPeekIssue } = useIssueDetail();
|
const { peekIssue, setPeekIssue } = useIssueDetail();
|
||||||
const { captureIssueEvent } = useEventTracker();
|
|
||||||
// states
|
// states
|
||||||
const [isMenuActive, setIsMenuActive] = useState(false);
|
const [isMenuActive, setIsMenuActive] = useState(false);
|
||||||
const [isExpanded, setExpanded] = useState<boolean>(false);
|
const [isExpanded, setExpanded] = useState<boolean>(false);
|
||||||
@ -106,11 +106,12 @@ export const SpreadsheetIssueRow = observer((props: Props) => {
|
|||||||
{/* first column/ issue name and key column */}
|
{/* first column/ issue name and key column */}
|
||||||
<td
|
<td
|
||||||
className={cn(
|
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,
|
"border-b-[0.5px]": peekIssue?.issueId !== issueDetail.id,
|
||||||
}
|
}
|
||||||
)}
|
)}
|
||||||
|
tabIndex={0}
|
||||||
>
|
>
|
||||||
<WithDisplayPropertiesHOC displayProperties={displayProperties} displayPropertyKey="key">
|
<WithDisplayPropertiesHOC displayProperties={displayProperties} displayPropertyKey="key">
|
||||||
<div
|
<div
|
||||||
@ -149,11 +150,14 @@ export const SpreadsheetIssueRow = observer((props: Props) => {
|
|||||||
href={`/${workspaceSlug}/projects/${issueDetail.project_id}/issues/${issueId}`}
|
href={`/${workspaceSlug}/projects/${issueDetail.project_id}/issues/${issueId}`}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
onClick={() => handleIssuePeekOverview(issueDetail)}
|
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">
|
<div className="w-full overflow-hidden">
|
||||||
<Tooltip tooltipHeading="Title" tooltipContent={issueDetail.name}>
|
<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}
|
{issueDetail.name}
|
||||||
</div>
|
</div>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
@ -161,40 +165,16 @@ export const SpreadsheetIssueRow = observer((props: Props) => {
|
|||||||
</ControlLink>
|
</ControlLink>
|
||||||
</td>
|
</td>
|
||||||
{/* Rest of the columns */}
|
{/* Rest of the columns */}
|
||||||
{SPREADSHEET_PROPERTY_LIST.map((property) => {
|
{SPREADSHEET_PROPERTY_LIST.map((property) => (
|
||||||
const { Column } = SPREADSHEET_PROPERTY_DETAILS[property];
|
<IssueColumn
|
||||||
|
|
||||||
const shouldRenderProperty = property === "estimate" ? isEstimateEnabled : true;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<WithDisplayPropertiesHOC
|
|
||||||
displayProperties={displayProperties}
|
displayProperties={displayProperties}
|
||||||
displayPropertyKey={property}
|
issueDetail={issueDetail}
|
||||||
shouldRenderProperty={shouldRenderProperty}
|
disableUserActions={disableUserActions}
|
||||||
>
|
property={property}
|
||||||
<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">
|
handleIssues={handleIssues}
|
||||||
<Column
|
isEstimateEnabled={isEstimateEnabled}
|
||||||
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>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</tr>
|
</tr>
|
||||||
|
|
||||||
{isExpanded &&
|
{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";
|
import { SPREADSHEET_PROPERTY_LIST } from "constants/spreadsheet";
|
||||||
// components
|
// components
|
||||||
import { WithDisplayPropertiesHOC } from "../properties/with-display-properties-HOC";
|
import { WithDisplayPropertiesHOC } from "../properties/with-display-properties-HOC";
|
||||||
import { SpreadsheetHeaderColumn } from "./columns/header-column";
|
import { SpreadsheetHeaderColumn } from "./spreadsheet-header-column";
|
||||||
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
displayProperties: IIssueDisplayProperties;
|
displayProperties: IIssueDisplayProperties;
|
||||||
@ -22,7 +21,10 @@ export const SpreadsheetHeader = (props: Props) => {
|
|||||||
return (
|
return (
|
||||||
<thead className="sticky top-0 left-0 z-[1] border-b-[0.5px] border-custom-border-100">
|
<thead className="sticky top-0 left-0 z-[1] border-b-[0.5px] border-custom-border-100">
|
||||||
<tr>
|
<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">
|
<WithDisplayPropertiesHOC displayProperties={displayProperties} displayPropertyKey="key">
|
||||||
<span className="flex h-full w-24 flex-shrink-0 items-center px-4 py-2.5">
|
<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
|
<span className="mr-1.5 text-custom-text-400">#</span>ID
|
||||||
@ -34,25 +36,15 @@ export const SpreadsheetHeader = (props: Props) => {
|
|||||||
</span>
|
</span>
|
||||||
</th>
|
</th>
|
||||||
|
|
||||||
{SPREADSHEET_PROPERTY_LIST.map((property) => {
|
{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
|
<SpreadsheetHeaderColumn
|
||||||
|
property={property}
|
||||||
|
displayProperties={displayProperties}
|
||||||
displayFilters={displayFilters}
|
displayFilters={displayFilters}
|
||||||
handleDisplayFilterUpdate={handleDisplayFilterUpdate}
|
handleDisplayFilterUpdate={handleDisplayFilterUpdate}
|
||||||
property={property}
|
isEstimateEnabled={isEstimateEnabled}
|
||||||
/>
|
/>
|
||||||
</th>
|
))}
|
||||||
</WithDisplayPropertiesHOC>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
);
|
);
|
||||||
|
@ -5,6 +5,7 @@ import { EIssueActions } from "../types";
|
|||||||
//components
|
//components
|
||||||
import { SpreadsheetIssueRow } from "./issue-row";
|
import { SpreadsheetIssueRow } from "./issue-row";
|
||||||
import { SpreadsheetHeader } from "./spreadsheet-header";
|
import { SpreadsheetHeader } from "./spreadsheet-header";
|
||||||
|
import { useTableKeyboardNavigation } from "hooks/use-table-keyboard-navigation";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
displayProperties: IIssueDisplayProperties;
|
displayProperties: IIssueDisplayProperties;
|
||||||
@ -35,8 +36,10 @@ export const SpreadsheetTable = observer((props: Props) => {
|
|||||||
canEditProperties,
|
canEditProperties,
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
|
const handleKeyBoardNavigation = useTableKeyboardNavigation();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<table className="overflow-y-auto">
|
<table className="overflow-y-auto" onKeyDown={handleKeyBoardNavigation}>
|
||||||
<SpreadsheetHeader
|
<SpreadsheetHeader
|
||||||
displayProperties={displayProperties}
|
displayProperties={displayProperties}
|
||||||
displayFilters={displayFilters}
|
displayFilters={displayFilters}
|
||||||
|
@ -28,6 +28,7 @@ export const SPREADSHEET_PROPERTY_DETAILS: {
|
|||||||
icon: FC<ISvgIcons>;
|
icon: FC<ISvgIcons>;
|
||||||
Column: React.FC<{
|
Column: React.FC<{
|
||||||
issue: TIssue;
|
issue: TIssue;
|
||||||
|
onClose: () => void;
|
||||||
onChange: (issue: TIssue, data: Partial<TIssue>, updates: any) => void;
|
onChange: (issue: TIssue, data: Partial<TIssue>, updates: any) => void;
|
||||||
disabled: boolean;
|
disabled: boolean;
|
||||||
}>;
|
}>;
|
||||||
|
@ -1,23 +1,31 @@
|
|||||||
import { useCallback } from "react";
|
import { useCallback } from "react";
|
||||||
|
|
||||||
type TUseDropdownKeyDown = {
|
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, stopPropagation = true) => {
|
||||||
|
const stopEventPropagation = (event: React.KeyboardEvent<HTMLElement>) => {
|
||||||
|
if (stopPropagation) {
|
||||||
|
event.stopPropagation();
|
||||||
|
event.preventDefault();
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useDropdownKeyDown: TUseDropdownKeyDown = (onEnterKeyDown, onEscKeyDown) => {
|
|
||||||
const handleKeyDown = useCallback(
|
const handleKeyDown = useCallback(
|
||||||
(event: React.KeyboardEvent<HTMLElement>) => {
|
(event: React.KeyboardEvent<HTMLElement>) => {
|
||||||
if (event.key === "Enter") {
|
if (event.key === "Enter") {
|
||||||
event.stopPropagation();
|
stopEventPropagation(event);
|
||||||
event.preventDefault();
|
|
||||||
onEnterKeyDown();
|
onEnterKeyDown();
|
||||||
} else if (event.key === "Escape") {
|
} else if (event.key === "Escape") {
|
||||||
event.stopPropagation();
|
stopEventPropagation(event);
|
||||||
event.preventDefault();
|
|
||||||
onEscKeyDown();
|
onEscKeyDown();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[onEnterKeyDown, onEscKeyDown]
|
[onEnterKeyDown, onEscKeyDown, stopEventPropagation]
|
||||||
);
|
);
|
||||||
|
|
||||||
return handleKeyDown;
|
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