Skip to content

Commit a1184ff

Browse files
Address PR review: Blueprint Tag chips, alignment, and arrow guard.
Use Tag with getNodeTagStyles for filter chips while keeping a custom input for ghost Tab completion. Fix header alignment, Roam-safe styling, remove unnecessary memos, and guard ArrowDown when there are no results. Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent f209f11 commit a1184ff

2 files changed

Lines changed: 76 additions & 81 deletions

File tree

apps/roam/src/components/AdvancedNodeSearchDialog/AdvancedSearchDialog.tsx

Lines changed: 25 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -357,7 +357,9 @@ const AdvancedNodeSearchDialog = ({
357357
if (event.defaultPrevented) return;
358358
if (event.key === "ArrowDown" && results.length) {
359359
event.preventDefault();
360-
setActiveIndex((index) => Math.min(index + 1, results.length - 1));
360+
setActiveIndex((index) =>
361+
Math.min(Math.max(index, 0) + 1, results.length - 1),
362+
);
361363
} else if (event.key === "ArrowUp" && results.length) {
362364
event.preventDefault();
363365
setActiveIndex((index) => Math.max(index - 1, 0));
@@ -422,20 +424,22 @@ const AdvancedNodeSearchDialog = ({
422424
onMouseUp={(event) => event.stopPropagation()}
423425
className="flex min-h-0 flex-1 flex-col overflow-hidden"
424426
>
425-
<div className="flex flex-none items-start gap-2 border-b border-gray-200 px-3 py-2">
427+
<div className="flex flex-none items-center gap-2 border-b border-gray-200 px-3 py-2">
426428
<div className="flex min-w-0 flex-1 items-center rounded border border-gray-300 bg-white px-2 py-1">
427429
<Icon icon="search" size={16} className="mr-2 text-gray-500" />
428430
<NodeTypeChipsSearchInput
429431
inputRef={inputRef}
430432
nodeTypes={discourseNodes}
431-
onArrowDown={() =>
433+
onArrowDown={() => {
434+
if (!results.length) return;
432435
setActiveIndex((index) =>
433-
Math.min(index + 1, results.length - 1),
434-
)
435-
}
436-
onArrowUp={() =>
437-
setActiveIndex((index) => Math.max(index - 1, 0))
438-
}
436+
Math.min(Math.max(index, 0) + 1, results.length - 1),
437+
);
438+
}}
439+
onArrowUp={() => {
440+
if (!results.length) return;
441+
setActiveIndex((index) => Math.max(index - 1, 0));
442+
}}
439443
onCmdEnter={() => void onInsert()}
440444
onEnter={() => void onOpen()}
441445
onEscape={() => {
@@ -449,23 +453,19 @@ const AdvancedNodeSearchDialog = ({
449453
selectedTypeIds={selectedNodeTypeIds}
450454
/>
451455
</div>
452-
<div className="self-start">
453-
<DiscourseNodeTypeFilter
454-
nodeTypes={discourseNodes}
455-
onPopoverOpenChange={setIsTypeFilterPopoverOpen}
456-
onSelectedTypeIdsChange={setSelectedNodeTypeIds}
457-
selectedTypeIds={selectedNodeTypeIds}
458-
/>
459-
</div>
460-
<div className="self-start">
461-
<DiscourseNodeSortControl
462-
disabled={isIndexLoading || indexError}
463-
onSortChange={handleSortChange}
464-
sort={sort}
465-
/>
466-
</div>
456+
<DiscourseNodeTypeFilter
457+
nodeTypes={discourseNodes}
458+
onPopoverOpenChange={setIsTypeFilterPopoverOpen}
459+
onSelectedTypeIdsChange={setSelectedNodeTypeIds}
460+
selectedTypeIds={selectedNodeTypeIds}
461+
/>
462+
<DiscourseNodeSortControl
463+
disabled={isIndexLoading || indexError}
464+
onSortChange={handleSortChange}
465+
sort={sort}
466+
/>
467467
<Button
468-
className="shrink-0 self-start"
468+
className="shrink-0"
469469
icon="cross"
470470
minimal
471471
onClick={onClose}

apps/roam/src/components/AdvancedNodeSearchDialog/NodeTypeChipsSearchInput.tsx

Lines changed: 51 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
import { Button, Classes, Icon } from "@blueprintjs/core";
2-
import React, { useEffect, useMemo, useRef, useState } from "react";
1+
import { Classes, Tag } from "@blueprintjs/core";
2+
import React, { useEffect, useRef, useState } from "react";
33
import { getNodeTagStyles } from "~/utils/getDiscourseNodeColors";
44
import { type DiscourseNode } from "~/utils/getDiscourseNodes";
55

@@ -18,6 +18,9 @@ type NodeTypeChipsSearchInputProps = {
1818
onEscape: () => void;
1919
};
2020

21+
const CHIP_LABEL_MAX_WIDTH = "10rem";
22+
const INPUT_MIN_WIDTH = "12ch";
23+
2124
const isPlainCharacterKey = (event: React.KeyboardEvent): boolean =>
2225
event.key.length === 1 && !event.altKey && !event.ctrlKey && !event.metaKey;
2326

@@ -48,6 +51,20 @@ const getBestPrefixMatch = ({
4851
return exactMatch ?? matches[0];
4952
};
5053

54+
const getCompletionSuffix = ({
55+
bestPrefixMatch,
56+
searchTerm,
57+
}: {
58+
bestPrefixMatch: DiscourseNode | null;
59+
searchTerm: string;
60+
}): string => {
61+
if (!bestPrefixMatch) return "";
62+
const normalizedQuery = searchTerm.trim();
63+
const nodeText = bestPrefixMatch.text;
64+
if (nodeText.toLowerCase() === normalizedQuery.toLowerCase()) return "";
65+
return nodeText.slice(normalizedQuery.length);
66+
};
67+
5168
export const NodeTypeChipsSearchInput = ({
5269
nodeTypes,
5370
searchTerm,
@@ -65,39 +82,21 @@ export const NodeTypeChipsSearchInput = ({
6582
const [focusedChipIndex, setFocusedChipIndex] = useState(-1);
6683
const chipRefs = useRef<(HTMLSpanElement | null)[]>([]);
6784

68-
const nodeTypeById = useMemo(
69-
() =>
70-
Object.fromEntries(
71-
nodeTypes.map((nodeType) => [nodeType.type, nodeType]),
72-
),
73-
[nodeTypes],
74-
);
85+
const nodeTypeById = Object.fromEntries(
86+
nodeTypes.map((nodeType) => [nodeType.type, nodeType]),
87+
) as Record<string, DiscourseNode | undefined>;
7588

76-
const selectedNodeTypes = useMemo(
77-
() =>
78-
selectedTypeIds
79-
.map((typeId) => nodeTypeById[typeId])
80-
.filter((nodeType): nodeType is DiscourseNode => !!nodeType),
81-
[nodeTypeById, selectedTypeIds],
82-
);
89+
const selectedNodeTypes = selectedTypeIds
90+
.map((typeId) => nodeTypeById[typeId])
91+
.filter((nodeType): nodeType is DiscourseNode => !!nodeType);
8392

84-
const bestPrefixMatch = useMemo(
85-
() =>
86-
getBestPrefixMatch({
87-
nodeTypes,
88-
query: searchTerm,
89-
selectedTypeIds,
90-
}),
91-
[nodeTypes, searchTerm, selectedTypeIds],
92-
);
93+
const bestPrefixMatch = getBestPrefixMatch({
94+
nodeTypes,
95+
query: searchTerm,
96+
selectedTypeIds,
97+
});
9398

94-
const completionSuffix = useMemo(() => {
95-
if (!bestPrefixMatch) return "";
96-
const normalizedQuery = searchTerm.trim();
97-
const nodeText = bestPrefixMatch.text;
98-
if (nodeText.toLowerCase() === normalizedQuery.toLowerCase()) return "";
99-
return nodeText.slice(normalizedQuery.length);
100-
}, [bestPrefixMatch, searchTerm]);
99+
const completionSuffix = getCompletionSuffix({ bestPrefixMatch, searchTerm });
101100

102101
useEffect(() => {
103102
if (focusedChipIndex < 0) return;
@@ -254,48 +253,44 @@ export const NodeTypeChipsSearchInput = ({
254253
onClick={() => setFocusedChipIndex(index)}
255254
onKeyDown={(event) => handleChipKeyDown(event, index)}
256255
style={{
256+
borderRadius: 3,
257257
boxShadow: isFocused
258258
? "0 0 0 2px rgba(95, 87, 192, 0.2)"
259259
: undefined,
260-
borderRadius: 3,
260+
display: "inline-flex",
261261
}}
262262
>
263-
<span
264-
className="inline-flex items-center gap-1.5 rounded px-2 py-1 text-xs"
263+
<Tag
264+
active={isFocused}
265+
htmlTitle={nodeType.text}
266+
minimal
267+
onRemove={(event) => {
268+
event.stopPropagation();
269+
onSelectedTypeIdsChange(
270+
selectedTypeIds.filter((_, chipIndex) => chipIndex !== index),
271+
);
272+
focusInput();
273+
}}
265274
style={getNodeTagStyles(
266275
nodeType.canvasSettings?.color ?? "#000000",
267276
)}
268277
>
269-
<span className="max-w-[10rem] truncate leading-4">
278+
<span
279+
className="truncate"
280+
style={{ maxWidth: CHIP_LABEL_MAX_WIDTH }}
281+
>
270282
{nodeType.text}
271283
</span>
272-
<Button
273-
className="!h-4 !min-h-0 !w-4 !min-w-0 !p-0"
274-
aria-label={`Remove ${nodeType.text} filter`}
275-
icon={<Icon icon="cross" size={10} />}
276-
minimal
277-
onClick={(event) => {
278-
event.stopPropagation();
279-
onSelectedTypeIdsChange(
280-
selectedTypeIds.filter(
281-
(_, chipIndex) => chipIndex !== index,
282-
),
283-
);
284-
focusInput();
285-
}}
286-
onMouseDown={(event) => event.preventDefault()}
287-
small
288-
/>
289-
</span>
284+
</Tag>
290285
</span>
291286
);
292287
})}
293-
<span className="relative min-w-[12ch] flex-1">
288+
<span className="relative flex-1" style={{ minWidth: INPUT_MIN_WIDTH }}>
294289
{completionSuffix && (
295290
<span className="pointer-events-none absolute inset-0 flex items-center overflow-hidden whitespace-nowrap text-sm">
296291
<span className="invisible">{searchTerm}</span>
297292
<span className="text-gray-400">{completionSuffix}</span>
298-
<span className="ml-2 rounded bg-gray-100 px-1 text-[10px] uppercase tracking-wide text-gray-500">
293+
<span className="ml-2 rounded bg-gray-100 px-1 text-xs uppercase tracking-wide text-gray-500">
299294
Tab
300295
</span>
301296
</span>

0 commit comments

Comments
 (0)