11import type { ReactNode } from "react"
2- import { useCallback } from "react"
32import { clsx } from "clsx"
43import { CheckboxIcon } from "@/app/conf/_design-system/pixelarticons/checkbox-icon"
54
@@ -16,194 +15,112 @@ interface CheckboxTreeProps {
1615 items : CheckboxTreeItem [ ]
1716 selectedValues : string [ ]
1817 onSelectionChange : ( next : string [ ] ) => void
19- emptyFallback ?: ReactNode
18+ depth ?: number
2019}
2120
2221export function CheckboxTree ( {
2322 items,
2423 selectedValues,
2524 onSelectionChange,
26- emptyFallback ,
25+ depth = 0 ,
2726} : CheckboxTreeProps ) {
28- const toggleValue = useCallback (
29- ( value : string ) => {
30- const next = selectedValues . includes ( value )
31- ? selectedValues . filter ( tag => tag !== value )
32- : [ ... selectedValues , value ]
33- onSelectionChange ( next )
34- } ,
35- [ selectedValues , onSelectionChange ] ,
36- )
27+ return (
28+ < div >
29+ { items . map ( item => {
30+ const isSelectable = Boolean ( item . value )
31+ const isDisabled = item . disabled
32+ const isChecked = isSelectable
33+ ? selectedValues . includes ( item . value ! )
34+ : false
35+ const checkboxId = `checkbox-tree- ${ item . id } `
3736
38- const renderTree = ( nodes : CheckboxTreeItem [ ] , depth : number ) : ReactNode => {
39- return nodes . map ( node => {
40- const isSelectable = Boolean ( node . value )
41- const isDisabled = node . disabled
42- const isChecked = isSelectable
43- ? selectedValues . includes ( node . value ! )
44- : false
45- const checkboxId = `checkbox-tree-${ node . id } `
37+ const toggleValue = ( value : string ) => {
38+ const next = selectedValues . includes ( value )
39+ ? selectedValues . filter ( tag => tag !== value )
40+ : [ ...selectedValues , value ]
41+ onSelectionChange ( next )
42+ }
4643
47- return (
48- < div key = { node . id } >
49- < div
50- className = "flex items-start gap-2 py-1"
51- style = { {
52- paddingInlineStart : depth > 0 ? ( depth - 1 ) * 16 : 0 ,
53- } }
54- >
55- { isSelectable ? (
56- < label
57- htmlFor = { checkboxId }
58- className = { clsx (
59- "flex grow items-center gap-2" ,
60- isDisabled
61- ? "cursor-not-allowed text-neu-500"
62- : "cursor-pointer" ,
63- ) }
64- aria-disabled = { isDisabled }
65- >
66- < span className = "flex shrink-0 items-center" >
67- < input
68- id = { checkboxId }
69- type = "checkbox"
70- checked = { isChecked }
71- onChange = { ( ) => {
72- if ( ! isDisabled ) toggleValue ( node . value ! )
73- } }
74- disabled = { isDisabled }
75- className = "peer sr-only"
76- />
77- < CheckboxIcon
78- checked = { isChecked }
79- className = { clsx (
80- "pointer-events-none size-5 transition-colors peer-focus-visible:outline peer-focus-visible:outline-2 peer-focus-visible:outline-offset-1 peer-focus-visible:outline-neu-900" ,
81- isDisabled ? "text-neu-300" : undefined ,
82- ) }
83- aria-hidden
84- />
85- </ span >
86- < span
44+ return (
45+ < div key = { item . id } >
46+ < div
47+ className = "flex items-start gap-2 py-1"
48+ style = { { paddingInlineStart : ( depth - 1 ) * 16 } }
49+ >
50+ { isSelectable ? (
51+ < label
52+ htmlFor = { checkboxId }
8753 className = { clsx (
88- "min-w-0 grow truncate text-left" ,
89- isDisabled ? "text-neu-500" : "text-neu-800" ,
54+ "flex grow items-center gap-2" ,
55+ isDisabled
56+ ? "cursor-not-allowed text-neu-500"
57+ : "cursor-pointer" ,
9058 ) }
59+ aria-disabled = { isDisabled }
9160 >
92- { node . label }
93- </ span >
94- { node . count ? ( // we intentionally don't display 0
61+ < span className = "flex shrink-0 items-center" >
62+ < input
63+ id = { checkboxId }
64+ type = "checkbox"
65+ checked = { isChecked }
66+ onChange = { ( ) => {
67+ if ( ! isDisabled ) toggleValue ( item . value ! )
68+ } }
69+ disabled = { isDisabled }
70+ className = "peer sr-only"
71+ />
72+ < CheckboxIcon
73+ checked = { isChecked }
74+ className = { clsx (
75+ "pointer-events-none size-5 peer-focus-visible:outline peer-focus-visible:outline-2 peer-focus-visible:outline-offset-1 peer-focus-visible:outline-neu-900" ,
76+ isDisabled ? "text-neu-300" : undefined ,
77+ ) }
78+ aria-hidden
79+ />
80+ </ span >
9581 < span
9682 className = { clsx (
97- "ml-auto shrink -0 text-xs " ,
98- isDisabled ? "text-neu-400 " : "text-neu-700 " ,
83+ "min-w -0 grow truncate text-left " ,
84+ isDisabled ? "text-neu-500 " : "text-neu-800 " ,
9985 ) }
10086 >
101- { node . count }
87+ { item . label }
10288 </ span >
103- ) : null }
104- </ label >
105- ) : (
106- < div
107- className = { clsx (
108- "typography-menu mt-4 text-sm xl:mt-10" ,
109- isDisabled ? "text-neu-500" : "text-neu-900" ,
110- ) }
111- aria-disabled = { isDisabled }
112- >
113- { node . label }
114- </ div >
115- ) }
116- </ div >
117-
118- { node . children && node . children . length > 0 ? (
119- < div > { renderTree ( node . children , depth + 1 ) } </ div >
120- ) : null }
121- </ div >
122- )
123- } )
124- }
125-
126- if ( items . length === 0 ) {
127- return (
128- < div className = "py-4 text-sm text-neu-500" >
129- { emptyFallback ?? "No matches" }
130- </ div >
131- )
132- }
133-
134- return < div > { renderTree ( items , 0 ) } </ div >
135- }
136- if ( selectedValues . includes ( value ) ) {
137- onSelectionChange ( selectedValues . filter ( tag => tag !== value ) )
138- } else {
139- onSelectionChange ( [ ...selectedValues , value ] )
140- }
141- }
142-
143- const renderTree = ( nodes : PreparedTree [ ] ) : ReactNode => {
144- return nodes . map ( node => {
145- const isSelectable = Boolean ( node . value )
146- const isChecked = isSelectable
147- ? selectedValues . includes ( node . value ! )
148- : false
149- const checkboxId = `checkbox-tree-${ node . id } `
89+ { item . count ? ( // we intentionally don't display 0
90+ < span
91+ className = { clsx (
92+ "ml-auto shrink-0 text-xs" ,
93+ isDisabled ? "text-neu-400" : "text-neu-700" ,
94+ ) }
95+ >
96+ { item . count }
97+ </ span >
98+ ) : null }
99+ </ label >
100+ ) : (
101+ < div
102+ className = { clsx (
103+ "typography-menu mt-4 text-sm xl:mt-10" ,
104+ isDisabled ? "text-neu-500" : "text-neu-900" ,
105+ ) }
106+ aria-disabled = { isDisabled }
107+ >
108+ { item . label }
109+ </ div >
110+ ) }
111+ </ div >
150112
151- return (
152- < div key = { node . id } >
153- < div
154- className = "flex items-start gap-2 py-1"
155- style = { { paddingInlineStart : ( node . depth - 1 ) * 16 } }
156- >
157- { isSelectable ? (
158- < label
159- htmlFor = { checkboxId }
160- className = "flex grow cursor-pointer items-center gap-2"
161- >
162- < span className = "flex shrink-0 items-center" >
163- < input
164- id = { checkboxId }
165- type = "checkbox"
166- checked = { isChecked }
167- onChange = { ( ) => toggleValue ( node . value ! ) }
168- className = "peer sr-only"
169- />
170- < CheckboxIcon
171- checked = { isChecked }
172- className = "pointer-events-none size-5 transition-colors peer-focus-visible:outline peer-focus-visible:outline-2 peer-focus-visible:outline-offset-1 peer-focus-visible:outline-neu-900"
173- aria-hidden
174- />
175- </ span >
176- < span className = "min-w-0 grow truncate text-left text-neu-800" >
177- { node . label }
178- </ span >
179- { node . count ? ( // we intentionally don't display 0
180- < span className = "ml-auto shrink-0 text-xs text-neu-700" >
181- { node . count }
182- </ span >
183- ) : null }
184- </ label >
185- ) : (
186- < div className = "typography-menu mt-4 text-sm text-neu-900 xl:mt-10" >
187- { node . label }
188- </ div >
189- ) }
113+ { item . children && item . children . length > 0 ? (
114+ < CheckboxTree
115+ items = { item . children }
116+ selectedValues = { selectedValues }
117+ onSelectionChange = { onSelectionChange }
118+ depth = { depth + 1 }
119+ />
120+ ) : null }
190121 </ div >
191-
192- { node . children && node . children . length > 0 ? (
193- < div > { renderTree ( node . children ) } </ div >
194- ) : null }
195- </ div >
196- )
197- } )
198- }
199-
200- if ( filteredTree . length === 0 ) {
201- return (
202- < div className = "py-4 text-sm text-neu-500" >
203- { emptyFallback ?? "No matches" }
204- </ div >
205- )
206- }
207-
208- return < div > { renderTree ( filteredTree ) } </ div >
122+ )
123+ } ) }
124+ </ div >
125+ )
209126}
0 commit comments