mirror of
https://github.com/devonthesofa/obsidian-note-status.git
synced 2026-07-22 12:30:24 +00:00
feat: add keyboard navigation and global typing to StatusSelectorGroupedByTag
This commit is contained in:
parent
c221d6b576
commit
bf6a1e967d
2 changed files with 114 additions and 6 deletions
|
|
@ -1,4 +1,4 @@
|
|||
import React, { useState } from "react";
|
||||
import React, { useState, useEffect, useRef } from "react";
|
||||
import { NoteStatus } from "@/types/noteStatus";
|
||||
import { SearchFilter } from "../atoms/SearchFilter";
|
||||
import { StatusChip } from "../atoms/StatusChip";
|
||||
|
|
@ -23,6 +23,9 @@ export const StatusSelectorGroupedByTag: React.FC<Props> = ({
|
|||
onSelectedState,
|
||||
}) => {
|
||||
const [searchFilter, setSearchFilter] = useState("");
|
||||
const [focusedIndex, setFocusedIndex] = useState(-1);
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const searchRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
const filteredStatuses = searchFilter
|
||||
? availableStatuses.filter((status) =>
|
||||
|
|
@ -38,9 +41,105 @@ export const StatusSelectorGroupedByTag: React.FC<Props> = ({
|
|||
onSelectedState(frontmatterTagName, status, "select");
|
||||
};
|
||||
|
||||
const handleKeyDown = (e: React.KeyboardEvent) => {
|
||||
switch (e.key) {
|
||||
case "ArrowDown":
|
||||
e.preventDefault();
|
||||
if (filteredStatuses.length > 0) {
|
||||
setFocusedIndex((prev) =>
|
||||
prev < filteredStatuses.length - 1 ? prev + 1 : 0,
|
||||
);
|
||||
}
|
||||
break;
|
||||
case "ArrowUp":
|
||||
e.preventDefault();
|
||||
if (filteredStatuses.length > 0) {
|
||||
setFocusedIndex((prev) =>
|
||||
prev > 0 ? prev - 1 : filteredStatuses.length - 1,
|
||||
);
|
||||
}
|
||||
break;
|
||||
case "Tab":
|
||||
if (!e.shiftKey) {
|
||||
e.preventDefault();
|
||||
if (filteredStatuses.length > 0) {
|
||||
setFocusedIndex((prev) =>
|
||||
prev < filteredStatuses.length - 1 ? prev + 1 : 0,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
e.preventDefault();
|
||||
if (filteredStatuses.length > 0) {
|
||||
setFocusedIndex((prev) =>
|
||||
prev > 0 ? prev - 1 : filteredStatuses.length - 1,
|
||||
);
|
||||
}
|
||||
}
|
||||
break;
|
||||
case "Enter":
|
||||
if (
|
||||
focusedIndex >= 0 &&
|
||||
focusedIndex < filteredStatuses.length
|
||||
) {
|
||||
e.preventDefault();
|
||||
const status = filteredStatuses[focusedIndex];
|
||||
const isSelected = currentStatuses.some(
|
||||
(s) => s.name === status.name,
|
||||
);
|
||||
if (isSelected) {
|
||||
handleRemoveStatus(status);
|
||||
} else {
|
||||
handleSelectStatus(status);
|
||||
}
|
||||
}
|
||||
break;
|
||||
case "Backspace":
|
||||
e.preventDefault();
|
||||
setSearchFilter((prev) => prev.slice(0, -1));
|
||||
if (searchRef.current) {
|
||||
searchRef.current.focus();
|
||||
}
|
||||
break;
|
||||
case "Escape":
|
||||
e.preventDefault();
|
||||
setSearchFilter("");
|
||||
break;
|
||||
default:
|
||||
if (
|
||||
e.key.length === 1 &&
|
||||
!e.ctrlKey &&
|
||||
!e.metaKey &&
|
||||
!e.altKey
|
||||
) {
|
||||
e.preventDefault();
|
||||
setSearchFilter((prev) => prev + e.key);
|
||||
if (searchRef.current) {
|
||||
searchRef.current.focus();
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
setFocusedIndex(filteredStatuses.length > 0 ? 0 : -1);
|
||||
}, [searchFilter, filteredStatuses.length]);
|
||||
|
||||
useEffect(() => {
|
||||
if (containerRef.current) {
|
||||
containerRef.current.focus();
|
||||
}
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div
|
||||
ref={containerRef}
|
||||
tabIndex={0}
|
||||
onKeyDown={handleKeyDown}
|
||||
style={{ outline: "none" }}
|
||||
>
|
||||
<SearchFilter
|
||||
ref={searchRef}
|
||||
value={searchFilter}
|
||||
onFilterChange={(value) => setSearchFilter(value)}
|
||||
/>
|
||||
|
|
@ -62,6 +161,7 @@ export const StatusSelectorGroupedByTag: React.FC<Props> = ({
|
|||
<StatusSelector
|
||||
availableStatuses={filteredStatuses}
|
||||
currentStatuses={currentStatuses}
|
||||
focusedIndex={focusedIndex}
|
||||
onToggleStatus={(status, selected) =>
|
||||
selected
|
||||
? handleSelectStatus(status)
|
||||
|
|
@ -70,7 +170,6 @@ export const StatusSelectorGroupedByTag: React.FC<Props> = ({
|
|||
/>
|
||||
)}
|
||||
</SettingItem>
|
||||
|
||||
<SettingItem name="Available statuses" vertical>
|
||||
<div
|
||||
className="note-status-chips"
|
||||
|
|
|
|||
|
|
@ -1,14 +1,17 @@
|
|||
import React, { useState } from "react";
|
||||
import { NoteStatus } from "@/types/noteStatus";
|
||||
|
||||
interface StatusOptionProps {
|
||||
status: NoteStatus;
|
||||
isSelected: boolean;
|
||||
isFocused: boolean;
|
||||
onSelect: () => void;
|
||||
}
|
||||
|
||||
export const StatusModalOption: React.FC<StatusOptionProps> = ({
|
||||
status,
|
||||
isSelected,
|
||||
isFocused,
|
||||
onSelect,
|
||||
}) => {
|
||||
const [isHovered, setIsHovered] = useState(false);
|
||||
|
|
@ -39,9 +42,13 @@ export const StatusModalOption: React.FC<StatusOptionProps> = ({
|
|||
borderBottom: "1px solid var(--background-modifier-border)",
|
||||
transition: "background-color 150ms ease",
|
||||
background:
|
||||
isSelected || isHovered
|
||||
isSelected || isHovered || isFocused
|
||||
? "var(--background-modifier-hover)"
|
||||
: "",
|
||||
outline: isFocused
|
||||
? "2px solid var(--interactive-accent)"
|
||||
: "none",
|
||||
outlineOffset: "-2px",
|
||||
}}
|
||||
>
|
||||
<span
|
||||
|
|
@ -90,12 +97,14 @@ export const StatusModalOption: React.FC<StatusOptionProps> = ({
|
|||
export interface Props {
|
||||
currentStatuses: NoteStatus[];
|
||||
availableStatuses: NoteStatus[];
|
||||
focusedIndex?: number;
|
||||
onToggleStatus: (status: NoteStatus, selected: boolean) => void;
|
||||
}
|
||||
|
||||
export const StatusSelector: React.FC<Props> = ({
|
||||
currentStatuses,
|
||||
availableStatuses,
|
||||
focusedIndex = -1,
|
||||
onToggleStatus,
|
||||
}) => {
|
||||
const handleSelectStatus = async (status: NoteStatus) => {
|
||||
|
|
@ -104,7 +113,6 @@ export const StatusSelector: React.FC<Props> = ({
|
|||
onToggleStatus(status, !selected);
|
||||
};
|
||||
|
||||
// TODO: The StatusSelector must be splitted by its template
|
||||
return (
|
||||
<div
|
||||
className="note-status-options"
|
||||
|
|
@ -116,7 +124,7 @@ export const StatusSelector: React.FC<Props> = ({
|
|||
background: "var(--background-primary)",
|
||||
}}
|
||||
>
|
||||
{availableStatuses.map((status) => (
|
||||
{availableStatuses.map((status, index) => (
|
||||
<StatusModalOption
|
||||
key={`${status.name}${status.description}${status.color}${status.icon}`}
|
||||
status={status}
|
||||
|
|
@ -125,6 +133,7 @@ export const StatusSelector: React.FC<Props> = ({
|
|||
(s) => s.name === status.name,
|
||||
) !== -1
|
||||
}
|
||||
isFocused={index === focusedIndex}
|
||||
onSelect={() => handleSelectStatus(status)}
|
||||
/>
|
||||
))}
|
||||
|
|
|
|||
Loading…
Reference in a new issue