[PWA-26] chore: pwa input focus improvement (#5507)

* chore: pwa dropdown input focus improvement

* chore: tab indices helper function updated and code refactor

* chore: modal tab index refactoring

* fix: PWA filters input autofocus

* chore: intake tab index updated and code refactor

* chore: code refactor
This commit is contained in:
Anmol Singh Bhatia 2024-09-06 16:21:14 +05:30 committed by GitHub
parent c84c37805c
commit 52f78a86af
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
47 changed files with 430 additions and 125 deletions

View file

@ -14,10 +14,12 @@ interface IInputSearch {
inputContainerClassName?: string;
inputClassName?: string;
inputPlaceholder?: string;
isMobile: boolean;
}
export const InputSearch: FC<IInputSearch> = (props) => {
const { isOpen, query, updateQuery, inputIcon, inputContainerClassName, inputClassName, inputPlaceholder } = props;
const { isOpen, query, updateQuery, inputIcon, inputContainerClassName, inputClassName, inputPlaceholder, isMobile } =
props;
const inputRef = useRef<HTMLInputElement | null>(null);
@ -29,10 +31,10 @@ export const InputSearch: FC<IInputSearch> = (props) => {
};
useEffect(() => {
if (isOpen) {
if (isOpen && !isMobile) {
inputRef.current && inputRef.current.focus();
}
}, [isOpen]);
}, [isOpen, isMobile]);
return (
<div
className={cn(

View file

@ -26,6 +26,7 @@ export const DropdownOptions: React.FC<IMultiSelectDropdownOptions | ISingleSele
value,
renderItem,
loader,
isMobile = false,
} = props;
return (
<>
@ -38,6 +39,7 @@ export const DropdownOptions: React.FC<IMultiSelectDropdownOptions | ISingleSele
inputPlaceholder={inputPlaceholder}
inputClassName={inputClassName}
inputContainerClassName={inputContainerClassName}
isMobile={isMobile}
/>
)}
<div className="mt-2 max-h-48 space-y-1 overflow-y-scroll">

View file

@ -85,6 +85,7 @@ export interface IDropdownOptions {
renderItem: (({ value, selected }: { value: string; selected: boolean }) => React.ReactNode) | undefined;
options: TDropdownOption[] | undefined;
loader?: React.ReactNode;
isMobile?: boolean;
}
export interface IMultiSelectDropdownOptions extends IDropdownOptions {

View file

@ -1,11 +1,25 @@
"use client";
import { FC } from "react";
import { Controller, useFormContext } from "react-hook-form";
import { IProject } from "@plane/types";
// ui
import { CustomSelect } from "@plane/ui";
// components
import { MemberDropdown } from "@/components/dropdowns";
// constants
import { NETWORK_CHOICES } from "@/constants/project";
import { ETabIndices } from "@/constants/tab-indices";
// helpers
import { getTabIndex } from "@/helpers/tab-indices.helper";
const ProjectAttributes = () => {
type Props = {
isMobile?: boolean;
};
const ProjectAttributes: FC<Props> = (props) => {
const { isMobile = false } = props;
const { control } = useFormContext<IProject>();
const { getIndex } = getTabIndex(ETabIndices.PROJECT_CREATE, isMobile);
return (
<div className="flex flex-wrap items-center gap-2">
<Controller
@ -15,7 +29,7 @@ const ProjectAttributes = () => {
const currentNetwork = NETWORK_CHOICES.find((n) => n.key === value);
return (
<div className="flex-shrink-0 h-7" tabIndex={4}>
<div className="flex-shrink-0 h-7" tabIndex={getIndex("network")}>
<CustomSelect
value={value}
onChange={onChange}
@ -35,7 +49,7 @@ const ProjectAttributes = () => {
className="h-full"
buttonClassName="h-full"
noChevron
tabIndex={4}
tabIndex={getIndex("network")}
>
{NETWORK_CHOICES.map((network) => (
<CustomSelect.Option key={network.key} value={network.key}>
@ -59,7 +73,7 @@ const ProjectAttributes = () => {
render={({ field: { value, onChange } }) => {
if (value === undefined || value === null || typeof value === "string")
return (
<div className="flex-shrink-0 h-7" tabIndex={5}>
<div className="flex-shrink-0 h-7" tabIndex={getIndex("lead")}>
<MemberDropdown
value={value}
onChange={(lead) => onChange(lead === value ? null : lead)}

View file

@ -120,7 +120,7 @@ export const CreateProjectForm: FC<Props> = observer((props) => {
return (
<FormProvider {...methods}>
<ProjectCreateHeader handleClose={handleClose} />
<ProjectCreateHeader handleClose={handleClose} isMobile={isMobile} />
<form onSubmit={handleSubmit(onSubmit)} className="px-3">
<div className="mt-9 space-y-6 pb-5">
@ -130,7 +130,7 @@ export const CreateProjectForm: FC<Props> = observer((props) => {
isChangeInIdentifierRequired={isChangeInIdentifierRequired}
setIsChangeInIdentifierRequired={setIsChangeInIdentifierRequired}
/>
<ProjectAttributes />
<ProjectAttributes isMobile={isMobile} />
</div>
<ProjectCreateButtons handleClose={handleClose} />
</form>

View file

@ -28,6 +28,8 @@ import { EmptyState } from "@/components/empty-state";
import { EmptyStateType } from "@/constants/empty-state";
// fetch-keys
import { ISSUE_DETAILS } from "@/constants/fetch-keys";
// helpers
import { getTabIndex } from "@/helpers/tab-indices.helper";
// hooks
import { useCommandPalette, useEventTracker, useProject, useUser } from "@/hooks/store";
import { useAppRouter } from "@/hooks/use-app-router";
@ -80,6 +82,8 @@ export const CommandModal: React.FC = observer(() => {
const debouncedSearchTerm = useDebounce(searchTerm, 500);
const { baseTabIndex } = getTabIndex(undefined, isMobile);
// TODO: update this to mobx store
const { data: issueDetails } = useSWR(
workspaceSlug && projectId && issueId ? ISSUE_DETAILS(issueId.toString()) : null,
@ -238,7 +242,7 @@ export const CommandModal: React.FC = observer(() => {
value={searchTerm}
onValueChange={(e) => setSearchTerm(e)}
autoFocus
tabIndex={1}
tabIndex={baseTabIndex}
/>
</div>

View file

@ -7,6 +7,8 @@ import { Combobox, Dialog, Transition } from "@headlessui/react";
import { ISearchIssueResponse, TProjectIssuesSearchParams } from "@plane/types";
// ui
import { Button, Loader, ToggleSwitch, Tooltip, TOAST_TYPE, setToast } from "@plane/ui";
// helpers
import { getTabIndex } from "@/helpers/tab-indices.helper";
// hooks
import useDebounce from "@/hooks/use-debounce";
import { usePlatformOS } from "@/hooks/use-platform-os";
@ -51,6 +53,7 @@ export const ExistingIssuesListModal: React.FC<Props> = (props) => {
const [isWorkspaceLevel, setIsWorkspaceLevel] = useState(false);
const { isMobile } = usePlatformOS();
const debouncedSearchTerm: string = useDebounce(searchTerm, 500);
const { baseTabIndex } = getTabIndex(undefined, isMobile);
const handleClose = () => {
onClose();
@ -140,6 +143,7 @@ export const ExistingIssuesListModal: React.FC<Props> = (props) => {
placeholder="Type to search..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
tabIndex={baseTabIndex}
/>
</div>

View file

@ -4,6 +4,7 @@ import { Search, X } from "lucide-react";
import { TCycleFilters, TCycleGroups } from "@plane/types";
// components
import { FilterEndDate, FilterStartDate, FilterStatus } from "@/components/cycles";
import { usePlatformOS } from "@/hooks/use-platform-os";
// types
type Props = {
@ -16,6 +17,8 @@ export const CycleFiltersSelection: React.FC<Props> = observer((props) => {
const { filters, handleFiltersUpdate, isArchived = false } = props;
// states
const [filtersSearchQuery, setFiltersSearchQuery] = useState("");
// hooks
const { isMobile } = usePlatformOS();
return (
<div className="flex h-full w-full flex-col overflow-hidden">
@ -28,7 +31,7 @@ export const CycleFiltersSelection: React.FC<Props> = observer((props) => {
placeholder="Search"
value={filtersSearchQuery}
onChange={(e) => setFiltersSearchQuery(e.target.value)}
autoFocus
autoFocus={!isMobile}
/>
{filtersSearchQuery !== "" && (
<button type="button" className="grid place-items-center" onClick={() => setFiltersSearchQuery("")}>

View file

@ -8,9 +8,12 @@ import { ICycle } from "@plane/types";
import { Button, Input, TextArea } from "@plane/ui";
// components
import { DateRangeDropdown, ProjectDropdown } from "@/components/dropdowns";
// constants
import { ETabIndices } from "@/constants/tab-indices";
// helpers
import { getDate, renderFormattedPayloadDate } from "@/helpers/date-time.helper";
import { shouldRenderProject } from "@/helpers/project.helper";
import { getTabIndex } from "@/helpers/tab-indices.helper";
type Props = {
handleFormSubmit: (values: Partial<ICycle>, dirtyFields: any) => Promise<void>;
@ -19,6 +22,7 @@ type Props = {
projectId: string;
setActiveProject: (projectId: string) => void;
data?: ICycle | null;
isMobile?: boolean;
};
const defaultValues: Partial<ICycle> = {
@ -29,7 +33,7 @@ const defaultValues: Partial<ICycle> = {
};
export const CycleForm: React.FC<Props> = (props) => {
const { handleFormSubmit, handleClose, status, projectId, setActiveProject, data } = props;
const { handleFormSubmit, handleClose, status, projectId, setActiveProject, data, isMobile = false } = props;
// form data
const {
formState: { errors, isSubmitting, dirtyFields },
@ -46,6 +50,8 @@ export const CycleForm: React.FC<Props> = (props) => {
},
});
const { getIndex } = getTabIndex(ETabIndices.PROJECT_CYCLE, isMobile);
useEffect(() => {
reset({
...defaultValues,
@ -71,7 +77,7 @@ export const CycleForm: React.FC<Props> = (props) => {
}}
buttonVariant="border-with-text"
renderCondition={(project) => shouldRenderProject(project)}
tabIndex={7}
tabIndex={getIndex("cover_image")}
/>
</div>
)}
@ -101,7 +107,7 @@ export const CycleForm: React.FC<Props> = (props) => {
inputSize="md"
onChange={onChange}
hasError={Boolean(errors?.name)}
tabIndex={1}
tabIndex={getIndex("description")}
autoFocus
/>
)}
@ -120,7 +126,7 @@ export const CycleForm: React.FC<Props> = (props) => {
hasError={Boolean(errors?.description)}
value={value}
onChange={onChange}
tabIndex={2}
tabIndex={getIndex("description")}
/>
)}
/>
@ -153,7 +159,7 @@ export const CycleForm: React.FC<Props> = (props) => {
hideIcon={{
to: true,
}}
tabIndex={3}
tabIndex={getIndex("date_range")}
/>
)}
/>
@ -163,10 +169,10 @@ export const CycleForm: React.FC<Props> = (props) => {
</div>
</div>
<div className="px-5 py-4 flex items-center justify-end gap-2 border-t-[0.5px] border-custom-border-200">
<Button variant="neutral-primary" size="sm" onClick={handleClose} tabIndex={4}>
<Button variant="neutral-primary" size="sm" onClick={handleClose} tabIndex={getIndex("cancel")}>
Cancel
</Button>
<Button variant="primary" size="sm" type="submit" loading={isSubmitting} tabIndex={5}>
<Button variant="primary" size="sm" type="submit" loading={isSubmitting} tabIndex={getIndex("submit")}>
{data ? (isSubmitting ? "Updating" : "Update Cycle") : isSubmitting ? "Creating" : "Create Cycle"}
</Button>
</div>

View file

@ -13,6 +13,7 @@ import { CYCLE_CREATED, CYCLE_UPDATED } from "@/constants/event-tracker";
// hooks
import { useEventTracker, useCycle, useProject } from "@/hooks/store";
import useLocalStorage from "@/hooks/use-local-storage";
import { usePlatformOS } from "@/hooks/use-platform-os";
// services
import { CycleService } from "@/services/cycle.service";
@ -35,6 +36,7 @@ export const CycleCreateUpdateModal: React.FC<CycleModalProps> = (props) => {
const { captureCycleEvent } = useEventTracker();
const { workspaceProjectIds } = useProject();
const { createCycle, updateCycleDetails } = useCycle();
const { isMobile } = usePlatformOS();
const { setValue: setCycleTab } = useLocalStorage<TCycleTabOptions>("cycle_tab", "active");
@ -186,6 +188,7 @@ export const CycleCreateUpdateModal: React.FC<CycleModalProps> = (props) => {
projectId={activeProject ?? ""}
setActiveProject={setActiveProject}
data={data}
isMobile={isMobile}
/>
</ModalCore>
);

View file

@ -14,6 +14,7 @@ import { TCycleGroups } from "@plane/types";
import { ContrastIcon, CycleGroupIcon } from "@plane/ui";
// store hooks
import { useCycle } from "@/hooks/store";
import { usePlatformOS } from "@/hooks/use-platform-os";
// types
type DropdownOptions =
@ -41,13 +42,16 @@ export const CycleOptions: FC<CycleOptionsProps> = observer((props) => {
// store hooks
const { workspaceSlug } = useParams();
const { getProjectCycleIds, fetchAllCycles, getCycleById } = useCycle();
const { isMobile } = usePlatformOS();
useEffect(() => {
if (isOpen) {
onOpen();
inputRef.current && inputRef.current.focus();
if (!isMobile) {
inputRef.current && inputRef.current.focus();
}
}
}, [isOpen]);
}, [isOpen, isMobile]);
// popper-js init
const { styles, attributes } = usePopper(referenceElement, popperElement, {

View file

@ -12,6 +12,7 @@ import { Combobox } from "@headlessui/react";
import { Avatar } from "@plane/ui";
//store
import { useUser, useMember } from "@/hooks/store";
import { usePlatformOS } from "@/hooks/use-platform-os";
interface Props {
projectId?: string;
@ -35,6 +36,7 @@ export const MemberOptions = observer((props: Props) => {
workspace: { workspaceMemberIds },
} = useMember();
const { data: currentUser } = useUser();
const { isMobile } = usePlatformOS();
// popper-js init
const { styles, attributes } = usePopper(referenceElement, popperElement, {
placement: placement ?? "bottom-start",
@ -51,9 +53,11 @@ export const MemberOptions = observer((props: Props) => {
useEffect(() => {
if (isOpen) {
onOpen();
inputRef.current && inputRef.current.focus();
if (!isMobile) {
inputRef.current && inputRef.current.focus();
}
}
}, [isOpen]);
}, [isOpen, isMobile]);
const memberIds = projectId ? getProjectMemberIds(projectId) : workspaceMemberIds;
const onOpen = () => {

View file

@ -185,6 +185,8 @@ export const ModuleDropdown: React.FC<Props> = observer((props) => {
const inputRef = useRef<HTMLInputElement | null>(null);
// popper-js refs
const [referenceElement, setReferenceElement] = useState<HTMLButtonElement | null>(null);
// store hooks
const { isMobile } = usePlatformOS();
const { getModuleNameById } = useModule();
@ -209,10 +211,10 @@ export const ModuleDropdown: React.FC<Props> = observer((props) => {
if (multiple) comboboxProps.multiple = true;
useEffect(() => {
if (isOpen && inputRef.current) {
if (isOpen && inputRef.current && !isMobile) {
inputRef.current.focus();
}
}, [isOpen]);
}, [isOpen, isMobile]);
const comboButton = (
<>

View file

@ -12,6 +12,7 @@ import { DiceIcon } from "@plane/ui";
//store
import { cn } from "@/helpers/common.helper";
import { useModule } from "@/hooks/store";
import { usePlatformOS } from "@/hooks/use-platform-os";
//hooks
//icon
//types
@ -42,14 +43,17 @@ export const ModuleOptions = observer((props: Props) => {
// store hooks
const { workspaceSlug } = useParams();
const { getProjectModuleIds, fetchModules, getModuleById } = useModule();
const { isMobile } = usePlatformOS();
useEffect(() => {
if (isOpen) {
onOpen();
inputRef.current && inputRef.current.focus();
if (!isMobile) {
inputRef.current && inputRef.current.focus();
}
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [isOpen]);
}, [isOpen, isMobile]);
// popper-js init
const { styles, attributes } = usePopper(referenceElement, popperElement, {

View file

@ -12,9 +12,11 @@ import {
} from "@/components/inbox/inbox-filter/filters";
// hooks
import { useMember, useLabel, useProjectState } from "@/hooks/store";
import { usePlatformOS } from "@/hooks/use-platform-os";
export const InboxIssueFilterSelection: FC = observer(() => {
// hooks
const { isMobile } = usePlatformOS();
const {
project: { projectMemberIds },
} = useMember();
@ -34,7 +36,7 @@ export const InboxIssueFilterSelection: FC = observer(() => {
placeholder="Search"
value={filtersSearchQuery}
onChange={(e) => setFiltersSearchQuery(e.target.value)}
autoFocus
autoFocus={!isMobile}
/>
{filtersSearchQuery !== "" && (
<button type="button" className="grid place-items-center" onClick={() => setFiltersSearchQuery("")}>

View file

@ -16,12 +16,15 @@ import {
} from "@/components/inbox/modals/create-edit-modal";
// constants
import { ISSUE_CREATED } from "@/constants/event-tracker";
import { ETabIndices } from "@/constants/tab-indices";
// helpers
import { renderFormattedPayloadDate } from "@/helpers/date-time.helper";
import { getTabIndex } from "@/helpers/tab-indices.helper";
// hooks
import { useEventTracker, useProjectInbox, useWorkspace } from "@/hooks/store";
import { useAppRouter } from "@/hooks/use-app-router";
import useKeypress from "@/hooks/use-keypress";
import { usePlatformOS } from "@/hooks/use-platform-os";
type TInboxIssueCreateRoot = {
workspaceSlug: string;
@ -53,6 +56,7 @@ export const InboxIssueCreateRoot: FC<TInboxIssueCreateRoot> = observer((props)
const { createInboxIssue } = useProjectInbox();
const { getWorkspaceBySlug } = useWorkspace();
const workspaceId = getWorkspaceBySlug(workspaceSlug)?.id;
const { isMobile } = usePlatformOS();
// states
const [createMore, setCreateMore] = useState<boolean>(false);
const [formSubmitting, setFormSubmitting] = useState(false);
@ -67,6 +71,8 @@ export const InboxIssueCreateRoot: FC<TInboxIssueCreateRoot> = observer((props)
[formData]
);
const { getIndex } = getTabIndex(ETabIndices.INTAKE_ISSUE_FORM, isMobile);
const handleEscKeyDown = (event: KeyboardEvent) => {
if (descriptionEditorRef.current?.isEditorReadyToDiscard()) {
handleModalClose();
@ -180,6 +186,7 @@ export const InboxIssueCreateRoot: FC<TInboxIssueCreateRoot> = observer((props)
className="inline-flex items-center gap-1.5 cursor-pointer"
onClick={() => setCreateMore((prevData) => !prevData)}
role="button"
tabIndex={getIndex("create_more")}
>
<ToggleSwitch value={createMore} onChange={() => {}} size="sm" />
<span className="text-xs">Create more</span>
@ -200,6 +207,7 @@ export const InboxIssueCreateRoot: FC<TInboxIssueCreateRoot> = observer((props)
});
}
}}
tabIndex={getIndex("discard_button")}
>
Discard
</Button>
@ -210,6 +218,7 @@ export const InboxIssueCreateRoot: FC<TInboxIssueCreateRoot> = observer((props)
type="submit"
loading={formSubmitting}
disabled={isTitleLengthMoreThan255Character}
tabIndex={getIndex("submit_button")}
>
{formSubmitting ? "Creating" : "Create Issue"}
</Button>

View file

@ -10,10 +10,14 @@ import { TIssue } from "@plane/types";
import { Loader } from "@plane/ui";
// components
import { RichTextEditor } from "@/components/editor/rich-text-editor/rich-text-editor";
// constants
import { ETabIndices } from "@/constants/tab-indices";
// helpers
import { getDescriptionPlaceholder } from "@/helpers/issue.helper";
import { getTabIndex } from "@/helpers/tab-indices.helper";
// hooks
import { useProjectInbox } from "@/hooks/store";
import { usePlatformOS } from "@/hooks/use-platform-os";
type TInboxIssueDescription = {
containerClassName?: string;
@ -32,6 +36,9 @@ export const InboxIssueDescription: FC<TInboxIssueDescription> = observer((props
props;
// hooks
const { loader } = useProjectInbox();
const { isMobile } = usePlatformOS();
const { getIndex } = getTabIndex(ETabIndices.INTAKE_ISSUE_FORM, isMobile);
if (loader === "issue-loading")
return (
@ -53,6 +60,7 @@ export const InboxIssueDescription: FC<TInboxIssueDescription> = observer((props
placeholder={getDescriptionPlaceholder}
containerClassName={containerClassName}
onEnterKeyPress={onEnterKeyPress}
tabIndex={getIndex("description_html")}
/>
);
});

View file

@ -15,10 +15,14 @@ import {
} from "@/components/dropdowns";
import { ParentIssuesListModal } from "@/components/issues";
import { IssueLabelSelect } from "@/components/issues/select";
// constants
import { ETabIndices } from "@/constants/tab-indices";
// helpers
import { renderFormattedPayloadDate, getDate } from "@/helpers/date-time.helper";
import { getTabIndex } from "@/helpers/tab-indices.helper";
// hooks
import { useProjectEstimates } from "@/hooks/store";
import { usePlatformOS } from "@/hooks/use-platform-os";
type TInboxIssueProperties = {
projectId: string;
@ -31,10 +35,12 @@ export const InboxIssueProperties: FC<TInboxIssueProperties> = observer((props)
const { projectId, data, handleData, isVisible = false } = props;
// hooks
const { areEstimateEnabledByProjectId } = useProjectEstimates();
const { isMobile } = usePlatformOS();
// states
const [parentIssueModalOpen, setParentIssueModalOpen] = useState(false);
const [selectedParentIssue, setSelectedParentIssue] = useState<ISearchIssueResponse | undefined>(undefined);
true;
const { getIndex } = getTabIndex(ETabIndices.INTAKE_ISSUE_FORM, isMobile);
const startDate = data?.start_date;
const targetDate = data?.target_date;
@ -54,6 +60,7 @@ export const InboxIssueProperties: FC<TInboxIssueProperties> = observer((props)
onChange={(stateId) => handleData("state_id", stateId)}
projectId={projectId}
buttonVariant="border-with-text"
tabIndex={getIndex("state_id")}
/>
</div>
@ -63,6 +70,7 @@ export const InboxIssueProperties: FC<TInboxIssueProperties> = observer((props)
value={data?.priority}
onChange={(priority) => handleData("priority", priority)}
buttonVariant="border-with-text"
tabIndex={getIndex("priority")}
/>
</div>
@ -76,6 +84,7 @@ export const InboxIssueProperties: FC<TInboxIssueProperties> = observer((props)
buttonClassName={(data?.assignee_ids || [])?.length > 0 ? "hover:bg-transparent" : ""}
placeholder="Assignees"
multiple
tabIndex={getIndex("assignee_ids")}
/>
</div>
@ -87,6 +96,7 @@ export const InboxIssueProperties: FC<TInboxIssueProperties> = observer((props)
value={data?.label_ids || []}
onChange={(labelIds) => handleData("label_ids", labelIds)}
projectId={projectId}
tabIndex={getIndex("label_ids")}
/>
</div>
@ -99,6 +109,7 @@ export const InboxIssueProperties: FC<TInboxIssueProperties> = observer((props)
buttonVariant="border-with-text"
minDate={minDate ?? undefined}
placeholder="Start date"
tabIndex={getIndex("start_date")}
/>
</div>
)}
@ -111,6 +122,7 @@ export const InboxIssueProperties: FC<TInboxIssueProperties> = observer((props)
buttonVariant="border-with-text"
minDate={minDate ?? undefined}
placeholder="Due date"
tabIndex={getIndex("target_date")}
/>
</div>
@ -123,6 +135,7 @@ export const InboxIssueProperties: FC<TInboxIssueProperties> = observer((props)
projectId={projectId}
placeholder="Cycle"
buttonVariant="border-with-text"
tabIndex={getIndex("cycle_id")}
/>
</div>
)}
@ -138,6 +151,7 @@ export const InboxIssueProperties: FC<TInboxIssueProperties> = observer((props)
buttonVariant="border-with-text"
multiple
showCount
tabIndex={getIndex("module_ids")}
/>
</div>
)}
@ -151,6 +165,7 @@ export const InboxIssueProperties: FC<TInboxIssueProperties> = observer((props)
projectId={projectId}
buttonVariant="border-with-text"
placeholder="Estimate"
tabIndex={getIndex("estimate_point")}
/>
</div>
)}
@ -174,6 +189,7 @@ export const InboxIssueProperties: FC<TInboxIssueProperties> = observer((props)
</button>
}
placement="bottom-start"
tabIndex={getIndex("parent_id")}
>
<>
<CustomMenu.MenuItem className="!p-1" onClick={() => setParentIssueModalOpen(true)}>

View file

@ -4,6 +4,12 @@ import { FC } from "react";
import { observer } from "mobx-react";
import { TIssue } from "@plane/types";
import { Input } from "@plane/ui";
// constants
import { ETabIndices } from "@/constants/tab-indices";
// helpers
import { getTabIndex } from "@/helpers/tab-indices.helper";
// hooks
import { usePlatformOS } from "@/hooks/use-platform-os";
type TInboxIssueTitle = {
data: Partial<TIssue>;
@ -13,7 +19,10 @@ type TInboxIssueTitle = {
export const InboxIssueTitle: FC<TInboxIssueTitle> = observer((props) => {
const { data, handleData, isTitleLengthMoreThan255Character } = props;
// hooks
const { isMobile } = usePlatformOS();
const { getIndex } = getTabIndex(ETabIndices.INTAKE_ISSUE_FORM, isMobile);
return (
<div className="space-y-1">
<Input
@ -24,6 +33,7 @@ export const InboxIssueTitle: FC<TInboxIssueTitle> = observer((props) => {
onChange={(e) => handleData("name", e.target.value)}
placeholder="Title"
className="w-full text-base"
tabIndex={getIndex("name")}
required
/>
{isTitleLengthMoreThan255Character && (

View file

@ -3,8 +3,11 @@ import { observer } from "mobx-react";
import { usePopper } from "react-popper";
import { Check, Search, Tag } from "lucide-react";
import { Combobox } from "@headlessui/react";
// helpers
import { getTabIndex } from "@/helpers/tab-indices.helper";
// hooks
import { useLabel } from "@/hooks/store";
import { usePlatformOS } from "@/hooks/use-platform-os";
// components
export interface IIssueLabelSelect {
@ -18,6 +21,7 @@ export interface IIssueLabelSelect {
export const IssueLabelSelect: React.FC<IIssueLabelSelect> = observer((props) => {
const { workspaceSlug, projectId, issueId, values, onSelect } = props;
// store hooks
const { isMobile } = usePlatformOS();
const { fetchProjectLabels, getProjectLabels } = useLabel();
// states
const [referenceElement, setReferenceElement] = useState<HTMLButtonElement | null>(null);
@ -27,6 +31,8 @@ export const IssueLabelSelect: React.FC<IIssueLabelSelect> = observer((props) =>
const projectLabels = getProjectLabels(projectId);
const { baseTabIndex } = getTabIndex(undefined, isMobile);
const fetchLabels = () => {
setIsLoading(true);
if (!projectLabels && workspaceSlug && projectId)
@ -123,6 +129,7 @@ export const IssueLabelSelect: React.FC<IIssueLabelSelect> = observer((props) =>
placeholder="Search"
displayValue={(assigned: any) => assigned?.name}
onKeyDown={searchInputKeyDown}
tabIndex={baseTabIndex}
/>
</div>
</div>

View file

@ -24,6 +24,7 @@ import {
import { ILayoutDisplayFiltersOptions } from "@/constants/issue";
// plane web components
import { FilterIssueTypes } from "@/plane-web/components/issues";
import { usePlatformOS } from "@/hooks/use-platform-os";
type Props = {
filters: IIssueFilterOptions;
@ -52,6 +53,7 @@ export const FilterSelection: React.FC<Props> = observer((props) => {
moduleViewDisabled = false,
} = props;
// hooks
const { isMobile } = usePlatformOS();
const { moduleId, cycleId } = useParams();
// states
const [filtersSearchQuery, setFiltersSearchQuery] = useState("");
@ -72,7 +74,7 @@ export const FilterSelection: React.FC<Props> = observer((props) => {
placeholder="Search"
value={filtersSearchQuery}
onChange={(e) => setFiltersSearchQuery(e.target.value)}
autoFocus
autoFocus={!isMobile}
/>
{filtersSearchQuery !== "" && (
<button type="button" className="grid place-items-center" onClick={() => setFiltersSearchQuery("")}>

View file

@ -109,10 +109,10 @@ export const IssuePropertyLabels: React.FC<IIssuePropertyLabels> = observer((pro
};
useEffect(() => {
if (isOpen && inputRef.current) {
if (isOpen && inputRef.current && !isMobile) {
inputRef.current.focus();
}
}, [isOpen]);
}, [isOpen, isMobile]);
const { styles, attributes } = usePopper(referenceElement, popperElement, {
placement: placement ?? "bottom-start",

View file

@ -20,11 +20,14 @@ import {
} from "@/components/dropdowns";
import { ParentIssuesListModal } from "@/components/issues";
import { IssueLabelSelect } from "@/components/issues/select";
// constants
import { ETabIndices } from "@/constants/tab-indices";
// helpers
import { getDate, renderFormattedPayloadDate } from "@/helpers/date-time.helper";
import { getTabIndex } from "@/helpers/issue-modal.helper";
import { getTabIndex } from "@/helpers/tab-indices.helper";
// hooks
import { useProjectEstimates, useProject } from "@/hooks/store";
import { usePlatformOS } from "@/hooks/use-platform-os";
// plane web components
import { IssueIdentifier } from "@/plane-web/components/issues";
@ -63,9 +66,12 @@ export const IssueDefaultProperties: React.FC<TIssueDefaultPropertiesProps> = ob
// store hooks
const { areEstimateEnabledByProjectId } = useProjectEstimates();
const { getProjectById } = useProject();
const { isMobile } = usePlatformOS();
// derived values
const projectDetails = getProjectById(projectId);
const { getIndex } = getTabIndex(ETabIndices.ISSUE_FORM, isMobile);
const minDate = getDate(startDate);
minDate?.setDate(minDate.getDate());
@ -87,7 +93,7 @@ export const IssueDefaultProperties: React.FC<TIssueDefaultPropertiesProps> = ob
}}
projectId={projectId ?? undefined}
buttonVariant="border-with-text"
tabIndex={getTabIndex("state_id")}
tabIndex={getIndex("state_id")}
/>
</div>
)}
@ -104,7 +110,7 @@ export const IssueDefaultProperties: React.FC<TIssueDefaultPropertiesProps> = ob
handleFormChange();
}}
buttonVariant="border-with-text"
tabIndex={getTabIndex("priority")}
tabIndex={getIndex("priority")}
/>
</div>
)}
@ -125,7 +131,7 @@ export const IssueDefaultProperties: React.FC<TIssueDefaultPropertiesProps> = ob
buttonClassName={value?.length > 0 ? "hover:bg-transparent" : ""}
placeholder="Assignees"
multiple
tabIndex={getTabIndex("assignee_ids")}
tabIndex={getIndex("assignee_ids")}
/>
</div>
)}
@ -143,7 +149,7 @@ export const IssueDefaultProperties: React.FC<TIssueDefaultPropertiesProps> = ob
handleFormChange();
}}
projectId={projectId ?? undefined}
tabIndex={getTabIndex("label_ids")}
tabIndex={getIndex("label_ids")}
/>
</div>
)}
@ -162,7 +168,7 @@ export const IssueDefaultProperties: React.FC<TIssueDefaultPropertiesProps> = ob
buttonVariant="border-with-text"
maxDate={maxDate ?? undefined}
placeholder="Start date"
tabIndex={getTabIndex("start_date")}
tabIndex={getIndex("start_date")}
/>
</div>
)}
@ -181,7 +187,7 @@ export const IssueDefaultProperties: React.FC<TIssueDefaultPropertiesProps> = ob
buttonVariant="border-with-text"
minDate={minDate ?? undefined}
placeholder="Due date"
tabIndex={getTabIndex("target_date")}
tabIndex={getIndex("target_date")}
/>
</div>
)}
@ -201,7 +207,7 @@ export const IssueDefaultProperties: React.FC<TIssueDefaultPropertiesProps> = ob
placeholder="Cycle"
value={value}
buttonVariant="border-with-text"
tabIndex={getTabIndex("cycle_id")}
tabIndex={getIndex("cycle_id")}
/>
</div>
)}
@ -222,7 +228,7 @@ export const IssueDefaultProperties: React.FC<TIssueDefaultPropertiesProps> = ob
}}
placeholder="Modules"
buttonVariant="border-with-text"
tabIndex={getTabIndex("module_ids")}
tabIndex={getIndex("module_ids")}
multiple
showCount
/>
@ -244,7 +250,7 @@ export const IssueDefaultProperties: React.FC<TIssueDefaultPropertiesProps> = ob
}}
projectId={projectId}
buttonVariant="border-with-text"
tabIndex={getTabIndex("estimate_point")}
tabIndex={getIndex("estimate_point")}
placeholder="Estimate"
/>
</div>
@ -270,7 +276,7 @@ export const IssueDefaultProperties: React.FC<TIssueDefaultPropertiesProps> = ob
</button>
}
placement="bottom-start"
tabIndex={getTabIndex("parent_id")}
tabIndex={getIndex("parent_id")}
>
<>
<CustomMenu.MenuItem className="!p-1" onClick={() => setParentIssueListModalOpen(true)}>

View file

@ -13,12 +13,15 @@ import { Loader, setToast, TOAST_TYPE } from "@plane/ui";
// components
import { GptAssistantPopover } from "@/components/core";
import { RichTextEditor } from "@/components/editor";
// constants
import { ETabIndices } from "@/constants/tab-indices";
// helpers
import { getTabIndex } from "@/helpers/issue-modal.helper";
import { getDescriptionPlaceholder } from "@/helpers/issue.helper";
import { getTabIndex } from "@/helpers/tab-indices.helper";
// hooks
import { useInstance, useWorkspace } from "@/hooks/store";
import useKeypress from "@/hooks/use-keypress";
import { usePlatformOS } from "@/hooks/use-platform-os";
// services
import { AIService } from "@/services/ai.service";
@ -63,6 +66,9 @@ export const IssueDescriptionEditor: React.FC<TIssueDescriptionEditorProps> = ob
const { getWorkspaceBySlug } = useWorkspace();
const workspaceId = getWorkspaceBySlug(workspaceSlug?.toString())?.id as string;
const { config } = useInstance();
const { isMobile } = usePlatformOS();
const { getIndex } = getTabIndex(ETabIndices.ISSUE_FORM, isMobile);
useEffect(() => {
if (descriptionHtmlData) handleDescriptionHTMLDataChange(descriptionHtmlData);
@ -171,7 +177,7 @@ export const IssueDescriptionEditor: React.FC<TIssueDescriptionEditorProps> = ob
}}
onEnterKeyPress={() => submitBtnRef?.current?.click()}
ref={editorRef}
tabIndex={getTabIndex("description_html")}
tabIndex={getIndex("description_html")}
placeholder={getDescriptionPlaceholder}
containerClassName="pt-3 min-h-[120px]"
/>
@ -186,7 +192,7 @@ export const IssueDescriptionEditor: React.FC<TIssueDescriptionEditorProps> = ob
}`}
onClick={handleAutoGenerateDescription}
disabled={iAmFeelingLucky}
tabIndex={getTabIndex("feeling_lucky")}
tabIndex={getIndex("feeling_lucky")}
>
{iAmFeelingLucky ? (
"Generating response"
@ -214,7 +220,7 @@ export const IssueDescriptionEditor: React.FC<TIssueDescriptionEditorProps> = ob
type="button"
className="flex items-center gap-1 rounded px-1.5 py-1 text-xs bg-custom-background-90 hover:bg-custom-background-80"
onClick={() => setGptAssistantModal((prevData) => !prevData)}
tabIndex={getTabIndex("ai_assistant")}
tabIndex={getIndex("ai_assistant")}
>
<Sparkle className="h-4 w-4" />
AI

View file

@ -6,8 +6,12 @@ import { Control, Controller } from "react-hook-form";
import { X } from "lucide-react";
// types
import { ISearchIssueResponse, TIssue } from "@plane/types";
// constants
import { ETabIndices } from "@/constants/tab-indices";
// helpers
import { getTabIndex } from "@/helpers/issue-modal.helper";
import { getTabIndex } from "@/helpers/tab-indices.helper";
// hooks
import { usePlatformOS } from "@/hooks/use-platform-os";
// plane web components
import { IssueIdentifier } from "@/plane-web/components/issues";
@ -20,6 +24,10 @@ type TIssueParentTagProps = {
export const IssueParentTag: React.FC<TIssueParentTagProps> = observer((props) => {
const { control, selectedParentIssue, handleFormChange, setSelectedParentIssue } = props;
// store hooks
const { isMobile } = usePlatformOS();
const { getIndex } = getTabIndex(ETabIndices.ISSUE_FORM, isMobile);
return (
<Controller
@ -54,7 +62,7 @@ export const IssueParentTag: React.FC<TIssueParentTagProps> = observer((props) =
handleFormChange();
setSelectedParentIssue(null);
}}
tabIndex={getTabIndex("remove_parent")}
tabIndex={getIndex("remove_parent")}
>
<X className="h-3 w-3 cursor-pointer" />
</button>

View file

@ -7,11 +7,14 @@ import { Control, Controller } from "react-hook-form";
import { TIssue } from "@plane/types";
// components
import { ProjectDropdown } from "@/components/dropdowns";
// constants
import { ETabIndices } from "@/constants/tab-indices";
// helpers
import { getTabIndex } from "@/helpers/issue-modal.helper";
import { shouldRenderProject } from "@/helpers/project.helper";
import { getTabIndex } from "@/helpers/tab-indices.helper";
// store hooks
import { useUser } from "@/hooks/store";
import { usePlatformOS } from "@/hooks/use-platform-os";
type TIssueProjectSelectProps = {
control: Control<TIssue>;
@ -23,6 +26,9 @@ export const IssueProjectSelect: React.FC<TIssueProjectSelectProps> = observer((
const { control, disabled = false, handleFormChange } = props;
// store hooks
const { projectsWithCreatePermissions } = useUser();
const { isMobile } = usePlatformOS();
const { getIndex } = getTabIndex(ETabIndices.ISSUE_FORM, isMobile);
return (
<Controller
@ -42,7 +48,7 @@ export const IssueProjectSelect: React.FC<TIssueProjectSelectProps> = observer((
}}
buttonVariant="border-with-text"
renderCondition={(project) => shouldRenderProject(project)}
tabIndex={getTabIndex("project_id")}
tabIndex={getIndex("project_id")}
disabled={disabled}
/>
</div>

View file

@ -7,8 +7,12 @@ import { Control, Controller, FieldErrors } from "react-hook-form";
import { TIssue } from "@plane/types";
// ui
import { Input } from "@plane/ui";
// constants
import { ETabIndices } from "@/constants/tab-indices";
// helpers
import { getTabIndex } from "@/helpers/issue-modal.helper";
import { getTabIndex } from "@/helpers/tab-indices.helper";
// hooks
import { usePlatformOS } from "@/hooks/use-platform-os";
type TIssueTitleInputProps = {
control: Control<TIssue>;
@ -19,6 +23,11 @@ type TIssueTitleInputProps = {
export const IssueTitleInput: React.FC<TIssueTitleInputProps> = observer((props) => {
const { control, issueTitleRef, errors, handleFormChange } = props;
// store hooks
const { isMobile } = usePlatformOS();
const { getIndex } = getTabIndex(ETabIndices.ISSUE_FORM, isMobile);
const validateWhitespace = (value: string) => {
if (value.trim() === "") {
return "Title is required";
@ -52,7 +61,7 @@ export const IssueTitleInput: React.FC<TIssueTitleInputProps> = observer((props)
hasError={Boolean(errors.name)}
placeholder="Title"
className="w-full text-base"
tabIndex={getTabIndex("name")}
tabIndex={getIndex("name")}
autoFocus
/>
)}

View file

@ -19,13 +19,15 @@ import {
IssueTitleInput,
} from "@/components/issues/issue-modal/components";
import { CreateLabelModal } from "@/components/labels";
import { ETabIndices } from "@/constants/tab-indices";
// helpers
import { cn } from "@/helpers/common.helper";
import { getTabIndex } from "@/helpers/issue-modal.helper";
import { getChangedIssuefields } from "@/helpers/issue.helper";
import { getTabIndex } from "@/helpers/tab-indices.helper";
// hooks
import { useIssueModal } from "@/hooks/context/use-issue-modal";
import { useIssueDetail, useProject, useProjectState } from "@/hooks/store";
import { usePlatformOS } from "@/hooks/use-platform-os";
import { useProjectIssueProperties } from "@/hooks/use-project-issue-properties";
// plane web components
import { IssueAdditionalProperties, IssueTypeSelect } from "@/plane-web/components/issues/issue-modal";
@ -88,6 +90,7 @@ export const IssueFormRoot: FC<IssueFormProps> = observer((props) => {
const { getProjectById } = useProject();
const { getIssueTypeIdOnProjectChange, getActiveAdditionalPropertiesLength, handlePropertyValuesValidation } =
useIssueModal();
const { isMobile } = usePlatformOS();
const {
issue: { getIssueById },
@ -116,6 +119,8 @@ export const IssueFormRoot: FC<IssueFormProps> = observer((props) => {
watch: watch,
});
const { getIndex } = getTabIndex(ETabIndices.ISSUE_FORM, isMobile);
//reset few fields on projectId change
useEffect(() => {
if (isDirty) {
@ -371,7 +376,7 @@ export const IssueFormRoot: FC<IssueFormProps> = observer((props) => {
onKeyDown={(e) => {
if (e.key === "Enter") onCreateMoreToggleChange(!isCreateMoreToggleEnabled);
}}
tabIndex={getTabIndex("create_more")}
tabIndex={getIndex("create_more")}
role="button"
>
<ToggleSwitch value={isCreateMoreToggleEnabled} onChange={() => {}} size="sm" />
@ -393,7 +398,7 @@ export const IssueFormRoot: FC<IssueFormProps> = observer((props) => {
});
}
}}
tabIndex={getTabIndex("discard_button")}
tabIndex={getIndex("discard_button")}
>
Discard
</Button>
@ -405,7 +410,7 @@ export const IssueFormRoot: FC<IssueFormProps> = observer((props) => {
size="sm"
loading={isSubmitting}
onClick={handleSubmit((data) => handleFormSubmit({ ...data, is_draft: false }))}
tabIndex={getTabIndex("draft_button")}
tabIndex={getIndex("draft_button")}
>
{isSubmitting ? "Moving" : "Move from draft"}
</Button>
@ -415,7 +420,7 @@ export const IssueFormRoot: FC<IssueFormProps> = observer((props) => {
size="sm"
loading={isSubmitting}
onClick={handleSubmit((data) => handleFormSubmit(data, true))}
tabIndex={getTabIndex("draft_button")}
tabIndex={getIndex("draft_button")}
>
{isSubmitting ? "Saving" : "Save as draft"}
</Button>
@ -428,7 +433,7 @@ export const IssueFormRoot: FC<IssueFormProps> = observer((props) => {
size="sm"
ref={submitBtnRef}
loading={isSubmitting}
tabIndex={isDraft ? getTabIndex("submit_button") : getTabIndex("draft_button")}
tabIndex={isDraft ? getIndex("submit_button") : getIndex("draft_button")}
>
{data?.id ? (isSubmitting ? "Updating" : "Update") : isSubmitting ? "Creating" : "Create"}
</Button>

View file

@ -19,6 +19,7 @@ import { usePlatformOS } from "@/hooks/use-platform-os";
import { IssueIdentifier } from "@/plane-web/components/issues";
// services
import { ProjectService } from "@/services/project";
import { getTabIndex } from "@/helpers/tab-indices.helper";
type Props = {
isOpen: boolean;
@ -50,6 +51,8 @@ export const ParentIssuesListModal: React.FC<Props> = ({
const { workspaceSlug } = useParams();
const { baseTabIndex } = getTabIndex(undefined, isMobile);
const handleClose = () => {
onClose();
setSearchTerm("");
@ -121,6 +124,7 @@ export const ParentIssuesListModal: React.FC<Props> = ({
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
displayValue={() => ""}
tabIndex={baseTabIndex}
/>
</div>
<div className="flex p-2 sm:justify-end">

View file

@ -12,6 +12,7 @@ import { cn } from "@/helpers/common.helper";
import { useLabel } from "@/hooks/store";
import { useDropdownKeyDown } from "@/hooks/use-dropdown-key-down";
import useOutsideClickDetector from "@/hooks/use-outside-click-detector";
import { usePlatformOS } from "@/hooks/use-platform-os";
type Props = {
setIsOpen: React.Dispatch<React.SetStateAction<boolean>>;
@ -41,6 +42,7 @@ export const IssueLabelSelect: React.FC<Props> = observer((props) => {
const { workspaceSlug } = useParams();
// store hooks
const { getProjectLabels, fetchProjectLabels } = useLabel();
const { isMobile } = usePlatformOS();
// states
const [query, setQuery] = useState("");
const [referenceElement, setReferenceElement] = useState<HTMLButtonElement | null>(null);
@ -91,10 +93,10 @@ export const IssueLabelSelect: React.FC<Props> = observer((props) => {
useOutsideClickDetector(dropdownRef, handleClose);
useEffect(() => {
if (isDropdownOpen && inputRef.current) {
if (isDropdownOpen && inputRef.current && !isMobile) {
inputRef.current.focus();
}
}, [isDropdownOpen]);
}, [isDropdownOpen, isMobile]);
return (
<Combobox

View file

@ -7,14 +7,18 @@ import { TwitterPicker } from "react-color";
import { Controller, useForm } from "react-hook-form";
import { ChevronDown } from "lucide-react";
import { Dialog, Popover, Transition } from "@headlessui/react";
import type { IIssueLabel, IState } from "@plane/types";
// hooks
import { Button, Input, TOAST_TYPE, setToast } from "@plane/ui";
import { LABEL_COLOR_OPTIONS, getRandomLabelColor } from "@/constants/label";
import { useLabel } from "@/hooks/store";
// ui
// types
import type { IIssueLabel, IState } from "@plane/types";
// ui
import { Button, Input, TOAST_TYPE, setToast } from "@plane/ui";
// constants
import { LABEL_COLOR_OPTIONS, getRandomLabelColor } from "@/constants/label";
import { ETabIndices } from "@/constants/tab-indices";
// helpers
import { getTabIndex } from "@/helpers/tab-indices.helper";
// hooks
import { useLabel } from "@/hooks/store";
import { usePlatformOS } from "@/hooks/use-platform-os";
// types
type Props = {
@ -35,6 +39,7 @@ export const CreateLabelModal: React.FC<Props> = observer((props) => {
const { workspaceSlug } = useParams();
// store hooks
const { createLabel } = useLabel();
const { isMobile } = usePlatformOS();
// form info
const {
formState: { errors, isSubmitting },
@ -48,6 +53,8 @@ export const CreateLabelModal: React.FC<Props> = observer((props) => {
defaultValues,
});
const { getIndex } = getTabIndex(ETabIndices.CREATE_LABEL, isMobile);
/**
* For setting focus on name input
*/
@ -183,7 +190,7 @@ export const CreateLabelModal: React.FC<Props> = observer((props) => {
value={value}
onChange={onChange}
ref={ref}
tabIndex={1}
tabIndex={getIndex("name")}
hasError={Boolean(errors.name)}
placeholder="Label title"
className="w-full resize-none text-xl"

View file

@ -8,6 +8,7 @@ import { TModuleDisplayFilters, TModuleFilters } from "@plane/types";
import { TModuleStatus } from "@plane/ui";
import { FilterOption } from "@/components/issues";
import { FilterLead, FilterMembers, FilterStartDate, FilterStatus, FilterTargetDate } from "@/components/modules";
import { usePlatformOS } from "@/hooks/use-platform-os";
// types
type Props = {
@ -30,6 +31,8 @@ export const ModuleFiltersSelection: React.FC<Props> = observer((props) => {
} = props;
// states
const [filtersSearchQuery, setFiltersSearchQuery] = useState("");
// store
const { isMobile } = usePlatformOS();
return (
<div className="flex h-full w-full flex-col overflow-hidden">
@ -42,7 +45,7 @@ export const ModuleFiltersSelection: React.FC<Props> = observer((props) => {
placeholder="Search"
value={filtersSearchQuery}
onChange={(e) => setFiltersSearchQuery(e.target.value)}
autoFocus
autoFocus={!isMobile}
/>
{filtersSearchQuery !== "" && (
<button type="button" className="grid place-items-center" onClick={() => setFiltersSearchQuery("")}>

View file

@ -8,9 +8,12 @@ import { Button, Input, TextArea } from "@plane/ui";
// components
import { DateRangeDropdown, ProjectDropdown, MemberDropdown } from "@/components/dropdowns";
import { ModuleStatusSelect } from "@/components/modules";
// constants
import { ETabIndices } from "@/constants/tab-indices";
// helpers
import { getDate, renderFormattedPayloadDate } from "@/helpers/date-time.helper";
import { shouldRenderProject } from "@/helpers/project.helper";
import { getTabIndex } from "@/helpers/tab-indices.helper";
// types
type Props = {
@ -20,6 +23,7 @@ type Props = {
projectId: string;
setActiveProject: React.Dispatch<React.SetStateAction<string | null>>;
data?: IModule;
isMobile?: boolean;
};
const defaultValues: Partial<IModule> = {
@ -31,7 +35,7 @@ const defaultValues: Partial<IModule> = {
};
export const ModuleForm: React.FC<Props> = (props) => {
const { handleFormSubmit, handleClose, status, projectId, setActiveProject, data } = props;
const { handleFormSubmit, handleClose, status, projectId, setActiveProject, data, isMobile = false } = props;
// form info
const {
formState: { errors, isSubmitting, dirtyFields },
@ -49,6 +53,8 @@ export const ModuleForm: React.FC<Props> = (props) => {
},
});
const { getIndex } = getTabIndex(ETabIndices.PROJECT_MODULE, isMobile);
const handleCreateUpdateModule = async (formData: Partial<IModule>) => {
await handleFormSubmit(formData, dirtyFields);
@ -82,7 +88,7 @@ export const ModuleForm: React.FC<Props> = (props) => {
}}
buttonVariant="border-with-text"
renderCondition={(project) => shouldRenderProject(project)}
tabIndex={10}
tabIndex={getIndex("cover_image")}
/>
</div>
)}
@ -112,7 +118,7 @@ export const ModuleForm: React.FC<Props> = (props) => {
hasError={Boolean(errors?.name)}
placeholder="Title"
className="w-full text-base"
tabIndex={1}
tabIndex={getIndex("name")}
autoFocus
/>
)}
@ -132,7 +138,7 @@ export const ModuleForm: React.FC<Props> = (props) => {
placeholder="Description"
className="w-full text-base resize-none min-h-24"
hasError={Boolean(errors?.description)}
tabIndex={2}
tabIndex={getIndex("description")}
/>
)}
/>
@ -164,14 +170,14 @@ export const ModuleForm: React.FC<Props> = (props) => {
hideIcon={{
to: true,
}}
tabIndex={3}
tabIndex={getIndex("date_range")}
/>
)}
/>
)}
/>
<div className="h-7">
<ModuleStatusSelect control={control} error={errors.status} tabIndex={4} />
<ModuleStatusSelect control={control} error={errors.status} tabIndex={getIndex("status")} />
</div>
<Controller
control={control}
@ -185,7 +191,7 @@ export const ModuleForm: React.FC<Props> = (props) => {
multiple={false}
buttonVariant="border-with-text"
placeholder="Lead"
tabIndex={5}
tabIndex={getIndex("lead")}
/>
</div>
)}
@ -203,7 +209,7 @@ export const ModuleForm: React.FC<Props> = (props) => {
buttonVariant={value && value.length > 0 ? "transparent-without-text" : "border-with-text"}
buttonClassName={value && value.length > 0 ? "hover:bg-transparent px-0" : ""}
placeholder="Members"
tabIndex={6}
tabIndex={getIndex("member_ids")}
/>
</div>
)}
@ -212,10 +218,10 @@ export const ModuleForm: React.FC<Props> = (props) => {
</div>
</div>
<div className="px-5 py-4 flex items-center justify-end gap-2 border-t-[0.5px] border-custom-border-200">
<Button variant="neutral-primary" size="sm" onClick={handleClose} tabIndex={7}>
<Button variant="neutral-primary" size="sm" onClick={handleClose} tabIndex={getIndex("cancel")}>
Cancel
</Button>
<Button variant="primary" size="sm" type="submit" loading={isSubmitting} tabIndex={8}>
<Button variant="primary" size="sm" type="submit" loading={isSubmitting} tabIndex={getIndex("submit")}>
{status ? (isSubmitting ? "Updating" : "Update Module") : isSubmitting ? "Creating" : "Create Module"}
</Button>
</div>

View file

@ -13,6 +13,7 @@ import { ModuleForm } from "@/components/modules";
import { MODULE_CREATED, MODULE_UPDATED } from "@/constants/event-tracker";
// hooks
import { useEventTracker, useModule, useProject } from "@/hooks/store";
import { usePlatformOS } from "@/hooks/use-platform-os";
type Props = {
isOpen: boolean;
@ -38,6 +39,7 @@ export const CreateUpdateModuleModal: React.FC<Props> = observer((props) => {
const { captureModuleEvent } = useEventTracker();
const { workspaceProjectIds } = useProject();
const { createModule, updateModuleDetails } = useModule();
const { isMobile } = usePlatformOS();
const handleClose = () => {
reset(defaultValues);
@ -149,6 +151,7 @@ export const CreateUpdateModuleModal: React.FC<Props> = observer((props) => {
projectId={activeProject ?? ""}
setActiveProject={setActiveProject}
data={data}
isMobile={isMobile}
/>
</ModalCore>
);

View file

@ -5,6 +5,7 @@ import { TPageFilterProps, TPageFilters } from "@plane/types";
// components
import { FilterCreatedBy, FilterCreatedDate } from "@/components/common/filters";
import { FilterOption } from "@/components/issues";
import { usePlatformOS } from "@/hooks/use-platform-os";
type Props = {
filters: TPageFilters;
@ -16,6 +17,8 @@ export const PageFiltersSelection: React.FC<Props> = observer((props) => {
const { filters, handleFiltersUpdate, memberIds } = props;
// states
const [filtersSearchQuery, setFiltersSearchQuery] = useState("");
// store
const { isMobile } = usePlatformOS();
const handleFilters = (key: keyof TPageFilterProps, value: boolean | string | string[]) => {
const newValues = filters.filters?.[key] ?? [];
@ -51,7 +54,7 @@ export const PageFiltersSelection: React.FC<Props> = observer((props) => {
placeholder="Search"
value={filtersSearchQuery}
onChange={(e) => setFiltersSearchQuery(e.target.value)}
autoFocus
autoFocus={!isMobile}
/>
{filtersSearchQuery !== "" && (
<button type="button" className="grid place-items-center" onClick={() => setFiltersSearchQuery("")}>

View file

@ -10,8 +10,10 @@ import { Logo } from "@/components/common";
// constants
import { AccessField } from "@/components/common/access-field";
import { EPageAccess, PAGE_ACCESS_SPECIFIERS } from "@/constants/page";
import { ETabIndices } from "@/constants/tab-indices";
// helpers
import { convertHexEmojiToDecimal } from "@/helpers/emoji.helper";
import { getTabIndex } from "@/helpers/tab-indices.helper";
// hooks
import { usePlatformOS } from "@/hooks/use-platform-os";
@ -30,6 +32,8 @@ export const PageForm: React.FC<Props> = (props) => {
const [isOpen, setIsOpen] = useState(false);
const [isSubmitting, setIsSubmitting] = useState(false);
const { getIndex } = getTabIndex(ETabIndices.PROJECT_PAGE, isMobile);
const handlePageFormSubmit = async (e: FormEvent<HTMLFormElement>) => {
e.preventDefault();
try {
@ -99,7 +103,7 @@ export const PageForm: React.FC<Props> = (props) => {
onChange={(e) => handleFormData("name", e.target.value)}
placeholder="Title"
className="w-full resize-none text-base"
tabIndex={1}
tabIndex={getIndex("name")}
required
autoFocus
/>
@ -122,7 +126,7 @@ export const PageForm: React.FC<Props> = (props) => {
</h6>
</div>
<div className="flex items-center justify-end gap-2">
<Button variant="neutral-primary" size="sm" onClick={handleModalClose} tabIndex={4}>
<Button variant="neutral-primary" size="sm" onClick={handleModalClose} tabIndex={getIndex("cancel")}>
Cancel
</Button>
<Button
@ -131,7 +135,7 @@ export const PageForm: React.FC<Props> = (props) => {
type="submit"
loading={isSubmitting}
disabled={isTitleLengthMoreThan255Character}
tabIndex={5}
tabIndex={getIndex("submit")}
>
{isSubmitting ? "Creating" : "Create Page"}
</Button>

View file

@ -2,8 +2,14 @@ import { ChangeEvent } from "react";
import { Controller, useFormContext, UseFormSetValue } from "react-hook-form";
import { Info } from "lucide-react";
import { cn } from "@plane/editor";
// ui
import { Input, TextArea, Tooltip } from "@plane/ui";
// constants
import { ETabIndices } from "@/constants/tab-indices";
// helpers
import { projectIdentifierSanitizer } from "@/helpers/project.helper";
import { getTabIndex } from "@/helpers/tab-indices.helper";
// plane-web types
import { TProject } from "@/plane-web/types/projects";
type Props = {
@ -19,6 +25,8 @@ const ProjectCommonAttributes: React.FC<Props> = (props) => {
control,
} = useFormContext<TProject>();
const { getIndex } = getTabIndex(ETabIndices.PROJECT_CREATE, isMobile);
const handleNameChange = (onChange: (...event: any[]) => void) => (e: ChangeEvent<HTMLInputElement>) => {
if (!isChangeInIdentifierRequired) {
onChange(e);
@ -58,7 +66,7 @@ const ProjectCommonAttributes: React.FC<Props> = (props) => {
hasError={Boolean(errors.name)}
placeholder="Project name"
className="w-full focus:border-blue-400"
tabIndex={1}
tabIndex={getIndex("name")}
/>
)}
/>
@ -94,7 +102,7 @@ const ProjectCommonAttributes: React.FC<Props> = (props) => {
className={cn("w-full text-xs focus:border-blue-400 pr-7", {
uppercase: value,
})}
tabIndex={2}
tabIndex={getIndex("identifier")}
/>
)}
/>
@ -121,7 +129,7 @@ const ProjectCommonAttributes: React.FC<Props> = (props) => {
onChange={onChange}
className="!h-24 text-sm focus:border-blue-400"
hasError={Boolean(errors?.description)}
tabIndex={3}
tabIndex={getIndex("description")}
/>
)}
/>

View file

@ -2,18 +2,26 @@ import { useState } from "react";
import { Controller, useFormContext } from "react-hook-form";
import { X } from "lucide-react";
import { IProject } from "@plane/types";
// ui
import { CustomEmojiIconPicker, EmojiIconPickerTypes, Logo } from "@plane/ui";
// components
import { ImagePickerPopover } from "@/components/core";
// constants
import { ETabIndices } from "@/constants/tab-indices";
// helpers
import { convertHexEmojiToDecimal } from "@/helpers/emoji.helper";
import { getTabIndex } from "@/helpers/tab-indices.helper";
type Props = {
handleClose: () => void;
isMobile?: boolean;
};
const ProjectCreateHeader: React.FC<Props> = (props) => {
const { handleClose } = props;
const { handleClose, isMobile = false } = props;
const { watch, control } = useFormContext<IProject>();
const [isOpen, setIsOpen] = useState(false);
const { getIndex } = getTabIndex(ETabIndices.PROJECT_CREATE, isMobile);
return (
<div className="group relative h-44 w-full rounded-lg bg-custom-background-80">
@ -26,7 +34,7 @@ const ProjectCreateHeader: React.FC<Props> = (props) => {
)}
<div className="absolute right-2 top-2 p-2">
<button data-posthog="PROJECT_MODAL_CLOSE" type="button" onClick={handleClose} tabIndex={8}>
<button data-posthog="PROJECT_MODAL_CLOSE" type="button" onClick={handleClose} tabIndex={getIndex("close")}>
<X className="h-5 w-5 text-white" />
</button>
</div>
@ -35,7 +43,13 @@ const ProjectCreateHeader: React.FC<Props> = (props) => {
name="cover_image"
control={control}
render={({ field: { value, onChange } }) => (
<ImagePickerPopover label="Change Cover" onChange={onChange} control={control} value={value} tabIndex={9} />
<ImagePickerPopover
label="Change Cover"
onChange={onChange}
control={control}
value={value}
tabIndex={getIndex("cover_image")}
/>
)}
/>
</div>

View file

@ -1,22 +1,31 @@
import { useFormContext } from "react-hook-form";
import { IProject } from "@plane/types";
// ui
import { Button } from "@plane/ui";
// constants
import { ETabIndices } from "@/constants/tab-indices";
// helpers
import { getTabIndex } from "@/helpers/tab-indices.helper";
type Props = {
handleClose: () => void;
isMobile?: boolean;
};
const ProjectCreateButtons: React.FC<Props> = (props) => {
const { handleClose } = props;
const { handleClose, isMobile = false } = props;
const {
formState: { isSubmitting },
} = useFormContext<IProject>();
const { getIndex } = getTabIndex(ETabIndices.PROJECT_CREATE, isMobile);
return (
<div className="flex justify-end gap-2 py-4 border-t border-custom-border-100">
<Button variant="neutral-primary" size="sm" onClick={handleClose} tabIndex={6}>
<Button variant="neutral-primary" size="sm" onClick={handleClose} tabIndex={getIndex("cancel")}>
Cancel
</Button>
<Button variant="primary" type="submit" size="sm" loading={isSubmitting} tabIndex={7}>
<Button variant="primary" type="submit" size="sm" loading={isSubmitting} tabIndex={getIndex("submit")}>
{isSubmitting ? "Creating" : "Create project"}
</Button>
</div>

View file

@ -5,6 +5,7 @@ import { TProjectDisplayFilters, TProjectFilters } from "@plane/types";
// components
import { FilterOption } from "@/components/issues";
import { FilterAccess, FilterCreatedDate, FilterLead, FilterMembers } from "@/components/project";
import { usePlatformOS } from "@/hooks/use-platform-os";
// types
type Props = {
@ -19,6 +20,8 @@ export const ProjectFiltersSelection: React.FC<Props> = observer((props) => {
const { displayFilters, filters, handleFiltersUpdate, handleDisplayFiltersUpdate, memberIds } = props;
// states
const [filtersSearchQuery, setFiltersSearchQuery] = useState("");
// store
const { isMobile } = usePlatformOS();
return (
<div className="flex h-full w-full flex-col overflow-hidden">
@ -31,7 +34,7 @@ export const ProjectFiltersSelection: React.FC<Props> = observer((props) => {
placeholder="Search"
value={filtersSearchQuery}
onChange={(e) => setFiltersSearchQuery(e.target.value)}
autoFocus
autoFocus={!isMobile}
/>
{filtersSearchQuery !== "" && (
<button type="button" className="grid place-items-center" onClick={() => setFiltersSearchQuery("")}>

View file

@ -8,6 +8,7 @@ import { FilterOption } from "@/components/issues";
// constants
import { EViewAccess } from "@/constants/views";
import { FilterByAccess } from "@/plane-web/components/views/filters/access-filter";
import { usePlatformOS } from "@/hooks/use-platform-os";
type Props = {
filters: TViewFilters;
@ -19,6 +20,8 @@ export const ViewFiltersSelection: React.FC<Props> = observer((props) => {
const { filters, handleFiltersUpdate, memberIds } = props;
// states
const [filtersSearchQuery, setFiltersSearchQuery] = useState("");
// store
const { isMobile } = usePlatformOS();
// handles filter update
const handleFilters = (key: keyof TViewFilterProps, value: boolean | string | EViewAccess | string[]) => {
@ -55,7 +58,7 @@ export const ViewFiltersSelection: React.FC<Props> = observer((props) => {
placeholder="Search"
value={filtersSearchQuery}
onChange={(e) => setFiltersSearchQuery(e.target.value)}
autoFocus
autoFocus={!isMobile}
/>
{filtersSearchQuery !== "" && (
<button type="button" className="grid place-items-center" onClick={() => setFiltersSearchQuery("")}>

View file

@ -13,12 +13,16 @@ import { Logo } from "@/components/common";
import { AppliedFiltersList, DisplayFiltersSelection, FilterSelection, FiltersDropdown } from "@/components/issues";
// constants
import { EIssueLayoutTypes, ISSUE_DISPLAY_FILTERS_BY_LAYOUT } from "@/constants/issue";
import { ETabIndices } from "@/constants/tab-indices";
import { EViewAccess } from "@/constants/views";
// helpers
import { convertHexEmojiToDecimal } from "@/helpers/emoji.helper";
import { getComputedDisplayFilters, getComputedDisplayProperties } from "@/helpers/issue.helper";
import { getTabIndex } from "@/helpers/tab-indices.helper";
// hooks
import { useLabel, useMember, useProject, useProjectState } from "@/hooks/store";
import { usePlatformOS } from "@/hooks/use-platform-os";
import { AccessController } from "@/plane-web/components/views/access-controller";
import { LayoutDropDown } from "../dropdowns/layout";
@ -48,6 +52,7 @@ export const ProjectViewForm: React.FC<Props> = observer((props) => {
const {
project: { projectMemberIds },
} = useMember();
const { isMobile } = usePlatformOS();
// form info
const {
control,
@ -62,6 +67,8 @@ export const ProjectViewForm: React.FC<Props> = observer((props) => {
const logoValue = watch("logo_props");
const { getIndex } = getTabIndex(ETabIndices.PROJECT_VIEW, isMobile);
const selectedFilters: IIssueFilterOptions = {};
Object.entries(watch("filters") ?? {}).forEach(([key, value]) => {
if (!value) return;
@ -194,7 +201,7 @@ export const ProjectViewForm: React.FC<Props> = observer((props) => {
hasError={Boolean(errors.name)}
placeholder="Title"
className="w-full text-base"
tabIndex={1}
tabIndex={getIndex("name")}
autoFocus
/>
)}
@ -215,7 +222,7 @@ export const ProjectViewForm: React.FC<Props> = observer((props) => {
hasError={Boolean(errors?.description)}
value={value}
onChange={onChange}
tabIndex={2}
tabIndex={getIndex("descriptions")}
/>
)}
/>
@ -243,7 +250,7 @@ export const ProjectViewForm: React.FC<Props> = observer((props) => {
control={control}
name="filters"
render={({ field: { onChange, value: filters } }) => (
<FiltersDropdown title="Filters" tabIndex={3}>
<FiltersDropdown title="Filters" tabIndex={getIndex("filters")}>
<FilterSelection
filters={filters ?? {}}
handleFiltersUpdate={(key, value) => {
@ -322,10 +329,10 @@ export const ProjectViewForm: React.FC<Props> = observer((props) => {
</div>
</div>
<div className="px-5 py-4 flex items-center justify-end gap-2 border-t-[0.5px] border-custom-border-200">
<Button variant="neutral-primary" size="sm" onClick={handleClose} tabIndex={4}>
<Button variant="neutral-primary" size="sm" onClick={handleClose} tabIndex={getIndex("cancel")}>
Cancel
</Button>
<Button variant="primary" size="sm" type="submit" tabIndex={5} loading={isSubmitting}>
<Button variant="primary" size="sm" type="submit" tabIndex={getIndex("submit")} loading={isSubmitting}>
{data ? (isSubmitting ? "Updating" : "Update View") : isSubmitting ? "Creating" : "Create View"}
</Button>
</div>

View file

@ -1,22 +0,0 @@
export const ISSUE_FORM_TAB_INDICES = [
"name",
"description_html",
"feeling_lucky",
"ai_assistant",
"state_id",
"priority",
"assignee_ids",
"label_ids",
"start_date",
"target_date",
"cycle_id",
"module_ids",
"estimate_point",
"parent_id",
"create_more",
"discard_button",
"draft_button",
"submit_button",
"project_id",
"remove_parent",
];

View file

@ -0,0 +1,94 @@
export const ISSUE_FORM_TAB_INDICES = [
"name",
"description_html",
"feeling_lucky",
"ai_assistant",
"state_id",
"priority",
"assignee_ids",
"label_ids",
"start_date",
"target_date",
"cycle_id",
"module_ids",
"estimate_point",
"parent_id",
"create_more",
"discard_button",
"draft_button",
"submit_button",
"project_id",
"remove_parent",
];
export const INTAKE_ISSUE_CREATE_FORM_TAB_INDICES = [
"name",
"description_html",
"state_id",
"priority",
"assignee_ids",
"label_ids",
"start_date",
"target_date",
"cycle_id",
"module_ids",
"estimate_point",
"parent_id",
"create_more",
"discard_button",
"submit_button",
];
export const CREATE_LABEL_TAB_INDICES = ["name", "color", "cancel", "submit"];
export const PROJECT_CREATE_TAB_INDICES = [
"name",
"identifier",
"description",
"network",
"lead",
"cancel",
"submit",
"close",
"cover_image",
"logo_props",
];
export const PROJECT_CYCLE_TAB_INDICES = ["name", "description", "date_range", "cancel", "submit", "project_id"];
export const PROJECT_MODULE_TAB_INDICES = [
"name",
"description",
"date_range",
"status",
"lead",
"member_ids",
"cancel",
"submit",
];
export const PROJECT_VIEW_TAB_INDICES = ["name", "description", "filters", "cancel", "submit"];
export const PROJECT_PAGE_TAB_INDICES = ["name", "public", "private", "cancel", "submit"];
export enum ETabIndices {
ISSUE_FORM = "issue-form",
INTAKE_ISSUE_FORM = "intake-issue-form",
CREATE_LABEL = "create-label",
PROJECT_CREATE = "project-create",
PROJECT_CYCLE = "project-cycle",
PROJECT_MODULE = "project-module",
PROJECT_VIEW = "project-view",
PROJECT_PAGE = "project-page",
}
export const TAB_INDEX_MAP: Record<ETabIndices, string[]> = {
[ETabIndices.ISSUE_FORM]: ISSUE_FORM_TAB_INDICES,
[ETabIndices.INTAKE_ISSUE_FORM]: INTAKE_ISSUE_CREATE_FORM_TAB_INDICES,
[ETabIndices.CREATE_LABEL]: CREATE_LABEL_TAB_INDICES,
[ETabIndices.PROJECT_CREATE]: PROJECT_CREATE_TAB_INDICES,
[ETabIndices.PROJECT_CYCLE]: PROJECT_CYCLE_TAB_INDICES,
[ETabIndices.PROJECT_MODULE]: PROJECT_MODULE_TAB_INDICES,
[ETabIndices.PROJECT_VIEW]: PROJECT_VIEW_TAB_INDICES,
[ETabIndices.PROJECT_PAGE]: PROJECT_PAGE_TAB_INDICES,
};

View file

@ -2,6 +2,7 @@ import { useEffect } from "react";
// hooks
import { useDropdownKeyDown } from "@/hooks/use-dropdown-key-down";
import useOutsideClickDetector from "@/hooks/use-outside-click-detector";
import { usePlatformOS } from "./use-platform-os";
type TArguments = {
dropdownRef: React.RefObject<HTMLDivElement>;
@ -17,6 +18,8 @@ type TArguments = {
export const useDropdown = (args: TArguments) => {
const { dropdownRef, inputRef, isOpen, onClose, onOpen, query, setIsOpen, setQuery } = args;
const { isMobile } = usePlatformOS();
/**
* @description clear the search input when the user presses the escape key, if the search input is not empty
* @param {React.KeyboardEvent<HTMLInputElement>} e
@ -62,10 +65,10 @@ export const useDropdown = (args: TArguments) => {
// focus the search input when the dropdown is open
useEffect(() => {
if (isOpen && inputRef?.current) {
if (isOpen && inputRef?.current && !isMobile) {
inputRef.current.focus();
}
}, [inputRef, isOpen]);
}, [inputRef, isOpen, isMobile]);
return {
handleClose,

View file

@ -1,3 +0,0 @@
import { ISSUE_FORM_TAB_INDICES } from "@/constants/issue-modal";
export const getTabIndex = (key: string) => ISSUE_FORM_TAB_INDICES.findIndex((tabIndex) => tabIndex === key) + 1;

View file

@ -0,0 +1,10 @@
import { ETabIndices, TAB_INDEX_MAP } from "@/constants/tab-indices";
export const getTabIndex = (type?: ETabIndices, isMobile: boolean = false) => {
const getIndex = (key: string) =>
isMobile ? undefined : type && TAB_INDEX_MAP[type].findIndex((tabIndex) => tabIndex === key) + 1;
const baseTabIndex = isMobile ? -1 : 1;
return { getIndex, baseTabIndex };
};