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" ;
33import { getNodeTagStyles } from "~/utils/getDiscourseNodeColors" ;
44import { 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+
2124const 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+
5168export 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