@@ -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
@@ -264,6 +272,16 @@ export type SelectTreeProps = {
264272 open ?: boolean ;
265273} ;
266274
275+ const getFilteredOptions = ( options : Tree , value : string ) => {
276+ const rx = new RegExp ( `^${ value } |\\s${ value } ` , "i" ) ;
277+ return value === ""
278+ ? options
279+ : ( pruneTree (
280+ options as HierarchyValue [ ] ,
281+ ( d ) => ! ! d . label . match ( rx )
282+ ) as Tree ) ;
283+ } ;
284+
267285function SelectTree ( {
268286 label,
269287 options,
@@ -278,20 +296,29 @@ function SelectTree({
278296} : SelectTreeProps ) {
279297 const [ openState , setOpenState ] = useState ( false ) ;
280298 const [ minMenuWidth , setMinMenuWidth ] = useState < number > ( ) ;
281-
282- const handleClick = useEventCallback ( ( ev : React . MouseEvent < HTMLElement > ) => {
283- setOpenState ( true ) ;
284- setMinMenuWidth ( ev . currentTarget . clientWidth ) ;
285- onOpen ?.( ) ;
286- } ) ;
287-
288- const handleClose = useEventCallback ( ( ) => {
289- setOpenState ( false ) ;
290- onClose ?.( ) ;
291- } ) ;
299+ const [ inputValue , setInputValue ] = useState ( "" ) ;
292300 const classes = useStyles ( { disabled, open } ) ;
301+ const [ filteredOptions , setFilteredOptions ] = useState ( options ) ;
293302
294303 const parentsRef = React . useRef ( { } as Record < NodeId , NodeId > ) ;
304+ const menuRef = React . useRef < PopoverActions > ( null ) ;
305+ const id = useId ( ) ;
306+ const inputRef = React . useRef < HTMLDivElement > ( ) ;
307+
308+ const defaultExpanded = useMemo ( ( ) => {
309+ if ( ! value && options . length > 0 ) {
310+ return options [ 0 ] . value ? [ options [ 0 ] . value ] : [ ] ;
311+ }
312+ const res = value ? [ value ] : [ ] ;
313+ let cur = value ;
314+ const parents = parentsRef . current ;
315+ while ( cur && parents [ cur ] ) {
316+ res . push ( parents [ cur ] ) ;
317+ cur = parents [ cur ] ;
318+ }
319+ return res ;
320+ } , [ value , options ] ) ;
321+ const [ expanded , setExpanded ] = useState ( defaultExpanded ) ;
295322
296323 const labelsByValue = useMemo ( ( ) => {
297324 parentsRef . current = { } as Record < NodeId , NodeId > ;
@@ -311,20 +338,50 @@ function SelectTree({
311338 return res ;
312339 } , [ options ] ) ;
313340
314- const defaultExpanded = useMemo ( ( ) => {
315- if ( ! value && options . length > 0 ) {
316- return options [ 0 ] . value ? [ options [ 0 ] . value ] : [ ] ;
317- }
318- const res = value ? [ value ] : [ ] ;
319- let cur = value ;
320- const parents = parentsRef . current ;
321- while ( cur && parents [ cur ] ) {
322- res . push ( parents [ cur ] ) ;
323- cur = parents [ cur ] ;
324- }
341+ const handleOpen = useEventCallback ( ( ) => {
342+ setOpenState ( true ) ;
343+ setMinMenuWidth ( inputRef . current ?. clientLeft ) ;
344+ onOpen ?.( ) ;
345+ } ) ;
325346
326- return res ;
327- } , [ value , options ] ) ;
347+ const handleClose = useEventCallback ( ( ) => {
348+ setOpenState ( false ) ;
349+ setInputValue ( "" ) ;
350+ setFilteredOptions ( getFilteredOptions ( options , "" ) ) ;
351+ setExpanded ( defaultExpanded ) ;
352+ onClose ?.( ) ;
353+ } ) ;
354+
355+ const handleInputChange : TextFieldProps [ "onChange" ] = useEvent ( ( ev ) => {
356+ const value = ev . currentTarget . value ;
357+ setInputValue ( value ) ;
358+ const filteredOptions = getFilteredOptions ( options , value ) ;
359+ setFilteredOptions ( filteredOptions ) ;
360+ setExpanded ( ( curExpanded ) => {
361+ const newExpanded = Array . from (
362+ new Set ( [
363+ ...curExpanded ,
364+ ...flattenTree ( filteredOptions as HierarchyValue [ ] ) . map (
365+ ( v ) => v . value
366+ ) ,
367+ ] )
368+ ) ;
369+ return newExpanded ;
370+ } ) ;
371+ } ) ;
372+
373+ const handleClickResetInput = useEvent ( ( ) => {
374+ const newValue = "" ;
375+ setInputValue ( newValue ) ;
376+ setFilteredOptions ( getFilteredOptions ( options , newValue ) ) ;
377+ setExpanded ( defaultExpanded ) ;
378+ } ) ;
379+
380+ const handleNodeToggle : TreeViewProps [ "onNodeToggle" ] = useEvent (
381+ ( _ev , nodeIds ) => {
382+ setExpanded ( nodeIds ) ;
383+ }
384+ ) ;
328385
329386 const handleNodeSelect = useEventCallback ( ( _ev , value : NodeId ) => {
330387 onChange ( { target : { value : value } } ) ;
@@ -377,15 +434,14 @@ function SelectTree({
377434 [ defaultExpanded , treeItemClasses , treeItemTransitionProps ]
378435 ) ;
379436
380- const menuRef = React . useRef < PopoverActions > ( null ) ;
381-
382- const paperProps = useMemo ( ( ) => {
383- return {
437+ const paperProps = useMemo (
438+ ( ) => ( {
384439 style : {
385- minWidth : minMenuWidth === undefined ? 0 : minMenuWidth ,
440+ minWidth : minMenuWidth ?? 0 ,
386441 } ,
387- } ;
388- } , [ minMenuWidth ] ) ;
442+ } ) ,
443+ [ minMenuWidth ]
444+ ) ;
389445
390446 const menuTransitionProps = useMemo (
391447 ( ) => ( {
@@ -403,8 +459,14 @@ function SelectTree({
403459 [ ]
404460 ) ;
405461
406- const id = useId ( ) ;
407- const inputRef = React . useRef < HTMLDivElement > ( ) ;
462+ const treeRef = useRef ( ) ;
463+ const handleKeyDown : React . HTMLAttributes < HTMLInputElement > [ "onKeyDown" ] =
464+ useEvent ( ( ev ) => {
465+ if ( ev . key === "Enter" || ev . key == " " ) {
466+ handleOpen ( ) ;
467+ ev . preventDefault ( ) ;
468+ }
469+ } ) ;
408470
409471 useEffect ( ( ) => {
410472 const inputNode = inputRef . current ;
@@ -428,10 +490,11 @@ function SelectTree({
428490 ref = { inputRef }
429491 size = "small"
430492 className = { classes . input }
431- onClick = { disabled ? undefined : handleClick }
493+ onClick = { disabled ? undefined : handleOpen }
494+ onKeyDown = { handleKeyDown }
432495 endAdornment = { < Icon className = { classes . icon } name = "caretDown" /> }
433496 />
434- < Menu
497+ < Popover
435498 anchorOrigin = { {
436499 vertical : "bottom" ,
437500 horizontal : "left" ,
@@ -443,17 +506,46 @@ function SelectTree({
443506 PaperProps = { paperProps }
444507 TransitionProps = { menuTransitionProps }
445508 >
446- < TreeView
447- defaultSelected = { value }
448- defaultExpanded = { defaultExpanded }
449- onNodeSelect = { handleNodeSelect }
450- defaultCollapseIcon = { < ExpandMoreIcon /> }
451- defaultExpandIcon = { < ChevronRightIcon /> }
452- sx = { { flexGrow : 1 , overflowY : "auto" , pb : 2 , "user-select" : "none" } }
453- >
454- { renderTreeContent ( options ) }
455- </ TreeView >
456- </ Menu >
509+ < TextField
510+ size = "small"
511+ value = { inputValue }
512+ sx = { { p : 1 , width : "100%" } }
513+ InputProps = { {
514+ autoFocus : true ,
515+ startAdornment : < Icon name = "search" size = { 16 } color = "#555" /> ,
516+ endAdornment : (
517+ < IconButton size = "small" onClick = { handleClickResetInput } >
518+ < Icon name = "close" size = { 16 } />
519+ </ IconButton >
520+ ) ,
521+ sx : { "& input" : { fontSize : "12px" } , pl : 1 , pr : 1 } ,
522+ } }
523+ onChange = { handleInputChange }
524+ />
525+ { filteredOptions . length === 0 ? (
526+ < Typography variant = "body2" sx = { { px : 2 , py : 4 } } >
527+ < Trans id = "No results" />
528+ </ Typography >
529+ ) : (
530+ < TreeView
531+ ref = { treeRef }
532+ defaultSelected = { value }
533+ expanded = { expanded }
534+ onNodeToggle = { handleNodeToggle }
535+ onNodeSelect = { handleNodeSelect }
536+ defaultCollapseIcon = { < ExpandMoreIcon /> }
537+ defaultExpandIcon = { < ChevronRightIcon /> }
538+ sx = { {
539+ flexGrow : 1 ,
540+ overflowY : "auto" ,
541+ pb : 2 ,
542+ "user-select" : "none" ,
543+ } }
544+ >
545+ { renderTreeContent ( filteredOptions ) }
546+ </ TreeView >
547+ ) }
548+ </ Popover >
457549 </ div >
458550 ) ;
459551}
0 commit comments