@@ -6,25 +6,30 @@ import MUITreeItem, {
66 TreeItemProps ,
77 useTreeItem ,
88} from "@mui/lab/TreeItem" ;
9- import TreeView from "@mui/lab/TreeView" ;
9+ import TreeView , { TreeViewProps } from "@mui/lab/TreeView" ;
1010import {
1111 Theme ,
12- Menu ,
12+ Popover ,
1313 PopoverActions ,
1414 useEventCallback ,
1515 OutlinedInput ,
1616 Typography ,
1717 Collapse ,
18+ TextField ,
19+ TextFieldProps ,
20+ IconButton ,
1821} from "@mui/material" ;
1922import { makeStyles } from "@mui/styles" ;
2023import useId from "@mui/utils/useId" ;
2124import clsx from "clsx" ;
22- import { useCallback , useMemo , useState } from "react" ;
25+ import { useCallback , useMemo , useRef , useState } from "react" ;
2326import * as React from "react" ;
2427import { useEffect } from "react" ;
2528
2629import { Label } from "@/components/form" ;
30+ import { HierarchyValue } from "@/graphql/resolver-types" ;
2731import { Icon } from "@/icons" ;
32+ import { flattenTree , pruneTree } from "@/rdf/tree-utils" ;
2833import useEvent from "@/utils/use-event" ;
2934
3035const useStyles = makeStyles < Theme , { disabled ?: boolean ; open ?: boolean } > (
@@ -242,12 +247,15 @@ const TreeItem = (props: TreeItemProps) => {
242247 return < MUITreeItem { ...props } ContentComponent = { TreeItemContent } /> ;
243248} ;
244249
245- type Tree = {
246- value : string ;
247- label : string ;
248- children ?: Tree ;
250+ type TreeHierachyValue = Omit <
251+ HierarchyValue ,
252+ "depth" | "dimensionIri" | "children"
253+ > & {
249254 selectable ?: boolean ;
250- } [ ] ;
255+ children ?: TreeHierachyValue [ ] ;
256+ } ;
257+
258+ type Tree = TreeHierachyValue [ ] ;
251259
252260type NodeId = string ;
253261
@@ -263,6 +271,16 @@ export type SelectTreeProps = {
263271 open ?: boolean ;
264272} ;
265273
274+ const getFilteredOptions = ( options : Tree , value : string ) => {
275+ const rx = new RegExp ( `^${ value } |\\s${ value } ` , "i" ) ;
276+ return value === ""
277+ ? options
278+ : ( pruneTree (
279+ options as HierarchyValue [ ] ,
280+ ( d ) => ! ! d . label . match ( rx )
281+ ) as Tree ) ;
282+ } ;
283+
266284function SelectTree ( {
267285 label,
268286 options,
@@ -276,20 +294,29 @@ function SelectTree({
276294} : SelectTreeProps ) {
277295 const [ openState , setOpenState ] = useState ( false ) ;
278296 const [ minMenuWidth , setMinMenuWidth ] = useState < number > ( ) ;
279-
280- const handleClick = useEventCallback ( ( ev : React . MouseEvent < HTMLElement > ) => {
281- setOpenState ( true ) ;
282- setMinMenuWidth ( ev . currentTarget . clientWidth ) ;
283- onOpen ?.( ) ;
284- } ) ;
285-
286- const handleClose = useEventCallback ( ( ) => {
287- setOpenState ( false ) ;
288- onClose ?.( ) ;
289- } ) ;
297+ const [ inputValue , setInputValue ] = useState ( "" ) ;
290298 const classes = useStyles ( { disabled, open } ) ;
299+ const [ filteredOptions , setFilteredOptions ] = useState ( options ) ;
291300
292301 const parentsRef = React . useRef ( { } as Record < NodeId , NodeId > ) ;
302+ const menuRef = React . useRef < PopoverActions > ( null ) ;
303+ const id = useId ( ) ;
304+ const inputRef = React . useRef < HTMLDivElement > ( ) ;
305+
306+ const defaultExpanded = useMemo ( ( ) => {
307+ if ( ! value && options . length > 0 ) {
308+ return options [ 0 ] . value ? [ options [ 0 ] . value ] : [ ] ;
309+ }
310+ const res = value ? [ value ] : [ ] ;
311+ let cur = value ;
312+ const parents = parentsRef . current ;
313+ while ( cur && parents [ cur ] ) {
314+ res . push ( parents [ cur ] ) ;
315+ cur = parents [ cur ] ;
316+ }
317+ return res ;
318+ } , [ value , options ] ) ;
319+ const [ expanded , setExpanded ] = useState ( defaultExpanded ) ;
293320
294321 const labelsByValue = useMemo ( ( ) => {
295322 parentsRef . current = { } as Record < NodeId , NodeId > ;
@@ -309,20 +336,50 @@ function SelectTree({
309336 return res ;
310337 } , [ options ] ) ;
311338
312- const defaultExpanded = useMemo ( ( ) => {
313- if ( ! value && options . length > 0 ) {
314- return options [ 0 ] . value ? [ options [ 0 ] . value ] : [ ] ;
315- }
316- const res = value ? [ value ] : [ ] ;
317- let cur = value ;
318- const parents = parentsRef . current ;
319- while ( cur && parents [ cur ] ) {
320- res . push ( parents [ cur ] ) ;
321- cur = parents [ cur ] ;
322- }
339+ const handleOpen = useEventCallback ( ( ) => {
340+ setOpenState ( true ) ;
341+ setMinMenuWidth ( inputRef . current ?. clientLeft ) ;
342+ onOpen ?.( ) ;
343+ } ) ;
323344
324- return res ;
325- } , [ value , options ] ) ;
345+ const handleClose = useEventCallback ( ( ) => {
346+ setOpenState ( false ) ;
347+ setInputValue ( "" ) ;
348+ setFilteredOptions ( getFilteredOptions ( options , "" ) ) ;
349+ setExpanded ( defaultExpanded ) ;
350+ onClose ?.( ) ;
351+ } ) ;
352+
353+ const handleInputChange : TextFieldProps [ "onChange" ] = useEvent ( ( ev ) => {
354+ const value = ev . currentTarget . value ;
355+ setInputValue ( value ) ;
356+ const filteredOptions = getFilteredOptions ( options , value ) ;
357+ setFilteredOptions ( filteredOptions ) ;
358+ setExpanded ( ( curExpanded ) => {
359+ const newExpanded = Array . from (
360+ new Set ( [
361+ ...curExpanded ,
362+ ...flattenTree ( filteredOptions as HierarchyValue [ ] ) . map (
363+ ( v ) => v . value
364+ ) ,
365+ ] )
366+ ) ;
367+ return newExpanded ;
368+ } ) ;
369+ } ) ;
370+
371+ const handleClickResetInput = useEvent ( ( ) => {
372+ const newValue = "" ;
373+ setInputValue ( newValue ) ;
374+ setFilteredOptions ( getFilteredOptions ( options , newValue ) ) ;
375+ setExpanded ( defaultExpanded ) ;
376+ } ) ;
377+
378+ const handleNodeToggle : TreeViewProps [ "onNodeToggle" ] = useEvent (
379+ ( _ev , nodeIds ) => {
380+ setExpanded ( nodeIds ) ;
381+ }
382+ ) ;
326383
327384 const handleNodeSelect = useEventCallback ( ( _ev , value : NodeId ) => {
328385 onChange ( { target : { value : value } } ) ;
@@ -375,15 +432,14 @@ function SelectTree({
375432 [ defaultExpanded , treeItemClasses , treeItemTransitionProps ]
376433 ) ;
377434
378- const menuRef = React . useRef < PopoverActions > ( null ) ;
379-
380- const paperProps = useMemo ( ( ) => {
381- return {
435+ const paperProps = useMemo (
436+ ( ) => ( {
382437 style : {
383- minWidth : minMenuWidth === undefined ? 0 : minMenuWidth ,
438+ minWidth : minMenuWidth ?? 0 ,
384439 } ,
385- } ;
386- } , [ minMenuWidth ] ) ;
440+ } ) ,
441+ [ minMenuWidth ]
442+ ) ;
387443
388444 const menuTransitionProps = useMemo (
389445 ( ) => ( {
@@ -401,8 +457,14 @@ function SelectTree({
401457 [ ]
402458 ) ;
403459
404- const id = useId ( ) ;
405- const inputRef = React . useRef < HTMLDivElement > ( ) ;
460+ const treeRef = useRef ( ) ;
461+ const handleKeyDown : React . HTMLAttributes < HTMLInputElement > [ "onKeyDown" ] =
462+ useEvent ( ( ev ) => {
463+ if ( ev . key === "Enter" || ev . key == " " ) {
464+ handleOpen ( ) ;
465+ ev . preventDefault ( ) ;
466+ }
467+ } ) ;
406468
407469 useEffect ( ( ) => {
408470 const inputNode = inputRef . current ;
@@ -426,10 +488,11 @@ function SelectTree({
426488 ref = { inputRef }
427489 size = "small"
428490 className = { classes . input }
429- onClick = { disabled ? undefined : handleClick }
491+ onClick = { disabled ? undefined : handleOpen }
492+ onKeyDown = { handleKeyDown }
430493 endAdornment = { < Icon className = { classes . icon } name = "caretDown" /> }
431494 />
432- < Menu
495+ < Popover
433496 anchorOrigin = { {
434497 vertical : "bottom" ,
435498 horizontal : "left" ,
@@ -441,17 +504,46 @@ function SelectTree({
441504 PaperProps = { paperProps }
442505 TransitionProps = { menuTransitionProps }
443506 >
444- < TreeView
445- defaultSelected = { value }
446- defaultExpanded = { defaultExpanded }
447- onNodeSelect = { handleNodeSelect }
448- defaultCollapseIcon = { < ExpandMoreIcon /> }
449- defaultExpandIcon = { < ChevronRightIcon /> }
450- sx = { { flexGrow : 1 , overflowY : "auto" , pb : 2 , "user-select" : "none" } }
451- >
452- { renderTreeContent ( options ) }
453- </ TreeView >
454- </ Menu >
507+ < TextField
508+ size = "small"
509+ value = { inputValue }
510+ sx = { { p : 1 , width : "100%" } }
511+ InputProps = { {
512+ autoFocus : true ,
513+ startAdornment : < Icon name = "search" size = { 16 } color = "#555" /> ,
514+ endAdornment : (
515+ < IconButton size = "small" onClick = { handleClickResetInput } >
516+ < Icon name = "close" size = { 16 } />
517+ </ IconButton >
518+ ) ,
519+ sx : { "& input" : { fontSize : "12px" } , pl : 1 , pr : 1 } ,
520+ } }
521+ onChange = { handleInputChange }
522+ />
523+ { filteredOptions . length === 0 ? (
524+ < Typography variant = "body2" sx = { { px : 2 , py : 4 } } >
525+ < Trans id = "No results" />
526+ </ Typography >
527+ ) : (
528+ < TreeView
529+ ref = { treeRef }
530+ defaultSelected = { value }
531+ expanded = { expanded }
532+ onNodeToggle = { handleNodeToggle }
533+ onNodeSelect = { handleNodeSelect }
534+ defaultCollapseIcon = { < ExpandMoreIcon /> }
535+ defaultExpandIcon = { < ChevronRightIcon /> }
536+ sx = { {
537+ flexGrow : 1 ,
538+ overflowY : "auto" ,
539+ pb : 2 ,
540+ "user-select" : "none" ,
541+ } }
542+ >
543+ { renderTreeContent ( filteredOptions ) }
544+ </ TreeView >
545+ ) }
546+ </ Popover >
455547 </ div >
456548 ) ;
457549}
0 commit comments