fix: search filter forward ref; rename grouped by tag component

This commit is contained in:
Aleix Soler 2025-07-15 18:58:43 +02:00
parent bf6a1e967d
commit 200e8b4abd
5 changed files with 187 additions and 136 deletions

View file

@ -1,9 +1,9 @@
import React from "react";
import { GroupedStatuses, NoteStatus } from "@/types/noteStatus";
import {
StatusSelectorGroupedByTag,
Props as SSGByTagProps,
} from "./StatusSelectorGroupedByTag";
StatusSelectorGroup,
Props as StatusSelectorGroupProps,
} from "./StatusSelectorGroup";
export interface Props {
currentStatuses: GroupedStatuses;
@ -28,7 +28,7 @@ export const ChangeStatusModal: React.FC<Props> = ({
}) => {
const currentStatuses = Object.entries(initialStatuses);
const handleSelectedState: SSGByTagProps["onSelectedState"] = (
const handleSelectedState: StatusSelectorGroupProps["onSelectedState"] = (
frontmatterTagName,
status,
action,
@ -45,7 +45,7 @@ export const ChangeStatusModal: React.FC<Props> = ({
<h1>Change note status {filesQuantity}</h1>
{currentStatuses.map(([frontmatterTagName, statusList]) => (
<StatusSelectorGroupedByTag
<StatusSelectorGroup
key={frontmatterTagName}
frontmatterTagName={frontmatterTagName}
availableStatuses={availableStatuses}

View file

@ -0,0 +1,37 @@
import React from "react";
import { NoteStatus } from "@/types/noteStatus";
import { StatusChip } from "../atoms/StatusChip";
import { SettingItem } from "../SettingsUI.tsx/SettingItem";
interface Props {
currentStatuses: NoteStatus[];
onRemoveStatus: (status: NoteStatus) => void;
}
export const CurrentStatusChips: React.FC<Props> = ({
currentStatuses,
onRemoveStatus,
}) => {
return (
<SettingItem name="Available statuses" vertical>
<div
className="note-status-chips"
style={{
display: "flex",
flexWrap: "wrap",
gap: "6px",
minHeight: "32px",
alignItems: "center",
}}
>
{currentStatuses.map((status) => (
<StatusChip
key={status.name}
status={status}
onRemove={() => onRemoveStatus(status)}
/>
))}
</div>
</SettingItem>
);
};

View file

@ -0,0 +1,96 @@
import React from "react";
import { NoteStatus } from "@/types/noteStatus";
import { SearchFilter } from "../atoms/SearchFilter";
import { StatusSelector } from "../atoms/StatusSelector";
import { SettingItem } from "../SettingsUI.tsx/SettingItem";
import { CurrentStatusChips } from "./CurrentStatusChips";
import { useKeyboardNavigation } from "./useKeyboardNavigation";
export interface Props {
frontmatterTagName: string;
currentStatuses: NoteStatus[];
availableStatuses: NoteStatus[];
onSelectedState: (
frontmatterTagName: string,
status: NoteStatus,
action: "select" | "unselected",
) => void;
}
export const StatusSelectorGroup: React.FC<Props> = ({
currentStatuses,
availableStatuses,
frontmatterTagName,
onSelectedState,
}) => {
const handleRemoveStatus = (status: NoteStatus) => {
onSelectedState(frontmatterTagName, status, "unselected");
};
const handleSelectStatus = (status: NoteStatus) => {
onSelectedState(frontmatterTagName, status, "select");
};
const {
focusedIndex,
searchFilter,
filteredStatuses,
containerRef,
searchRef,
handleKeyDown,
setSearchFilter,
} = useKeyboardNavigation({
availableStatuses,
currentStatuses,
onSelectStatus: handleSelectStatus,
onRemoveStatus: handleRemoveStatus,
});
return (
<div
ref={containerRef}
tabIndex={0}
onKeyDown={handleKeyDown}
style={{ outline: "none" }}
>
<SearchFilter
ref={searchRef}
value={searchFilter}
onFilterChange={setSearchFilter}
/>
<SettingItem name="Current statuses" vertical>
{filteredStatuses.length === 0 ? (
<div
style={{
padding: "16px",
textAlign: "center",
color: "var(--text-muted)",
fontStyle: "italic",
}}
>
{searchFilter
? `No statuses match "${searchFilter}"`
: "No statuses found"}
</div>
) : (
<StatusSelector
availableStatuses={filteredStatuses}
currentStatuses={currentStatuses}
focusedIndex={focusedIndex}
onToggleStatus={(status, selected) =>
selected
? handleSelectStatus(status)
: handleRemoveStatus(status)
}
/>
)}
</SettingItem>
<CurrentStatusChips
currentStatuses={currentStatuses}
onRemoveStatus={handleRemoveStatus}
/>
</div>
);
};

View file

@ -1,29 +1,21 @@
import React, { useState, useEffect, useRef } from "react";
import { useState, useEffect, useRef } from "react";
import { NoteStatus } from "@/types/noteStatus";
import { SearchFilter } from "../atoms/SearchFilter";
import { StatusChip } from "../atoms/StatusChip";
import { StatusSelector } from "../atoms/StatusSelector";
import { SettingItem } from "../SettingsUI.tsx/SettingItem";
export interface Props {
frontmatterTagName: string;
currentStatuses: NoteStatus[];
interface UseKeyboardNavigationProps {
availableStatuses: NoteStatus[];
onSelectedState: (
frontmatterTagName: string,
status: NoteStatus,
action: "select" | "unselected",
) => void;
currentStatuses: NoteStatus[];
onSelectStatus: (status: NoteStatus) => void;
onRemoveStatus: (status: NoteStatus) => void;
}
export const StatusSelectorGroupedByTag: React.FC<Props> = ({
currentStatuses,
export const useKeyboardNavigation = ({
availableStatuses,
frontmatterTagName,
onSelectedState,
}) => {
const [searchFilter, setSearchFilter] = useState("");
currentStatuses,
onSelectStatus,
onRemoveStatus,
}: UseKeyboardNavigationProps) => {
const [focusedIndex, setFocusedIndex] = useState(-1);
const [searchFilter, setSearchFilter] = useState("");
const containerRef = useRef<HTMLDivElement>(null);
const searchRef = useRef<HTMLInputElement>(null);
@ -33,14 +25,6 @@ export const StatusSelectorGroupedByTag: React.FC<Props> = ({
)
: availableStatuses;
const handleRemoveStatus = async (status: NoteStatus) => {
onSelectedState(frontmatterTagName, status, "unselected");
};
const handleSelectStatus = async (status: NoteStatus) => {
onSelectedState(frontmatterTagName, status, "select");
};
const handleKeyDown = (e: React.KeyboardEvent) => {
switch (e.key) {
case "ArrowDown":
@ -87,9 +71,9 @@ export const StatusSelectorGroupedByTag: React.FC<Props> = ({
(s) => s.name === status.name,
);
if (isSelected) {
handleRemoveStatus(status);
onRemoveStatus(status);
} else {
handleSelectStatus(status);
onSelectStatus(status);
}
}
break;
@ -131,65 +115,13 @@ export const StatusSelectorGroupedByTag: React.FC<Props> = ({
}
}, []);
return (
<div
ref={containerRef}
tabIndex={0}
onKeyDown={handleKeyDown}
style={{ outline: "none" }}
>
<SearchFilter
ref={searchRef}
value={searchFilter}
onFilterChange={(value) => setSearchFilter(value)}
/>
<SettingItem name="Current statuses" vertical>
{filteredStatuses.length === 0 ? (
<div
style={{
padding: "16px",
textAlign: "center",
color: "var(--text-muted)",
fontStyle: "italic",
}}
>
{searchFilter
? `No statuses match "${searchFilter}"`
: "No statuses found"}
</div>
) : (
<StatusSelector
availableStatuses={filteredStatuses}
currentStatuses={currentStatuses}
focusedIndex={focusedIndex}
onToggleStatus={(status, selected) =>
selected
? handleSelectStatus(status)
: handleRemoveStatus(status)
}
/>
)}
</SettingItem>
<SettingItem name="Available statuses" vertical>
<div
className="note-status-chips"
style={{
display: "flex",
flexWrap: "wrap",
gap: "6px",
minHeight: "32px",
alignItems: "center",
}}
>
{currentStatuses.map((s) => (
<StatusChip
key={s.name}
status={s}
onRemove={() => handleRemoveStatus(s)}
/>
))}
</div>
</SettingItem>
</div>
);
return {
focusedIndex,
searchFilter,
filteredStatuses,
containerRef,
searchRef,
handleKeyDown,
setSearchFilter,
};
};

View file

@ -1,46 +1,32 @@
import { FC, useEffect, useRef, useState } from "react";
import React from "react";
export type Props = {
interface Props {
value: string;
onFilterChange: (value: string) => void;
};
export const SearchFilter: FC<Props> = ({ value, onFilterChange }) => {
const [searchFilter, setSearchFilter] = useState(value);
const searchInputRef = useRef<HTMLInputElement>(null);
placeholder?: string;
}
useEffect(() => {
// TODO:: Focus on render?
// if (searchInputRef.current) {
// setTimeout(() => searchInputRef.current?.focus(), 100);
// }
//
setSearchFilter(value);
}, [value]);
export const SearchFilter = React.forwardRef<HTMLInputElement, Props>(
({ value, onFilterChange, placeholder = "Search..." }, ref) => {
return (
<input
ref={ref}
type="text"
value={value}
onChange={(e) => onFilterChange(e.target.value)}
placeholder={placeholder}
style={{
width: "100%",
padding: "8px 12px",
border: "1px solid var(--background-modifier-border)",
borderRadius: "4px",
backgroundColor: "var(--background-primary)",
color: "var(--text-normal)",
fontSize: "14px",
}}
/>
);
},
);
// TODO: Move the style to its css file
return (
<div className="setting-item">
<div className="setting-item-info">
<div className="setting-item-name">Filter statuses</div>
</div>
<div className="setting-item-control">
<input
ref={searchInputRef}
type="text"
placeholder="Search statuses..."
className="note-status-search-input"
value={searchFilter}
onChange={(e) => onFilterChange(e.target.value)}
style={{
width: "200px",
padding: "6px 12px",
border: "1px solid var(--background-modifier-border)",
borderRadius: "var(--radius-s)",
background: "var(--background-primary)",
color: "var(--text-normal)",
}}
/>
</div>
</div>
);
};
SearchFilter.displayName = "SearchFilter";