@skyhook-io/k8s-ui 1.4.1 → 1.4.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useState, useMemo, useEffect, useRef, forwardRef } from 'react'
|
|
1
|
+
import { useState, useMemo, useEffect, useRef, useCallback, forwardRef } from 'react'
|
|
2
2
|
import {
|
|
3
3
|
Search,
|
|
4
4
|
ChevronDown,
|
|
@@ -44,6 +44,9 @@ export interface ResourcesSidebarProps {
|
|
|
44
44
|
onNavigate?: (path: string) => void
|
|
45
45
|
/** Base path for generating navigation URLs (e.g., '/org/clusters/id/k8s-resources') */
|
|
46
46
|
basePath?: string
|
|
47
|
+
/** Called when a kind is selected via keyboard (Enter in the filter). Parent uses this
|
|
48
|
+
* to move focus to the next UI level (e.g., the table search input). */
|
|
49
|
+
onKindNavigated?: () => void
|
|
47
50
|
}
|
|
48
51
|
|
|
49
52
|
// Persisted across remounts so collapsed categories survive tab switches
|
|
@@ -93,6 +96,8 @@ interface ResourceTypeButtonProps {
|
|
|
93
96
|
resource: APIResource
|
|
94
97
|
count: number
|
|
95
98
|
isSelected: boolean
|
|
99
|
+
/** Keyboard-highlight state (arrow nav in the filter input). */
|
|
100
|
+
isHighlighted?: boolean
|
|
96
101
|
isForbidden?: boolean
|
|
97
102
|
isPinned?: boolean
|
|
98
103
|
onTogglePin?: () => void
|
|
@@ -100,7 +105,7 @@ interface ResourceTypeButtonProps {
|
|
|
100
105
|
}
|
|
101
106
|
|
|
102
107
|
const ResourceTypeButton = forwardRef<HTMLButtonElement, ResourceTypeButtonProps>(
|
|
103
|
-
function ResourceTypeButton({ resource, count, isSelected, isForbidden: forbidden, isPinned, onTogglePin, onClick }, ref) {
|
|
108
|
+
function ResourceTypeButton({ resource, count, isSelected, isHighlighted, isForbidden: forbidden, isPinned, onTogglePin, onClick }, ref) {
|
|
104
109
|
const Icon = getResourceIcon(resource.kind)
|
|
105
110
|
return (
|
|
106
111
|
<button
|
|
@@ -110,9 +115,11 @@ const ResourceTypeButton = forwardRef<HTMLButtonElement, ResourceTypeButtonProps
|
|
|
110
115
|
'w-full flex items-center gap-2 px-2 xl:px-3 py-1.5 rounded-lg text-sm transition-colors group/kind min-w-0',
|
|
111
116
|
isSelected
|
|
112
117
|
? 'selection-strong selection-text'
|
|
113
|
-
:
|
|
114
|
-
? '
|
|
115
|
-
:
|
|
118
|
+
: isHighlighted
|
|
119
|
+
? 'bg-theme-hover text-theme-text-primary'
|
|
120
|
+
: forbidden
|
|
121
|
+
? 'text-theme-text-disabled hover:bg-theme-elevated hover:text-theme-text-secondary'
|
|
122
|
+
: 'text-theme-text-secondary hover:bg-theme-elevated hover:text-theme-text-primary'
|
|
116
123
|
)}
|
|
117
124
|
>
|
|
118
125
|
<Icon className="w-4 h-4 shrink-0" />
|
|
@@ -172,6 +179,7 @@ export function ResourcesSidebar({
|
|
|
172
179
|
className,
|
|
173
180
|
onNavigate,
|
|
174
181
|
basePath,
|
|
182
|
+
onKindNavigated,
|
|
175
183
|
}: ResourcesSidebarProps) {
|
|
176
184
|
// Wraps kind selection to also navigate when basePath/onNavigate are provided
|
|
177
185
|
const selectKind = (kind: SelectedKindInfo) => {
|
|
@@ -195,6 +203,15 @@ export function ResourcesSidebar({
|
|
|
195
203
|
// Ref to selected sidebar item for scrolling into view on deeplink
|
|
196
204
|
const selectedSidebarRef = useRef<HTMLButtonElement>(null)
|
|
197
205
|
|
|
206
|
+
// Ref to kind search input — auto-focused on mount so users can type a kind name immediately
|
|
207
|
+
const kindSearchRef = useRef<HTMLInputElement>(null)
|
|
208
|
+
useEffect(() => {
|
|
209
|
+
// Only focus on fine-pointer devices to avoid popping up the virtual keyboard on touch
|
|
210
|
+
if (typeof window !== 'undefined' && window.matchMedia('(pointer: fine)').matches) {
|
|
211
|
+
kindSearchRef.current?.focus()
|
|
212
|
+
}
|
|
213
|
+
}, [])
|
|
214
|
+
|
|
198
215
|
// Effective selected kind — fall back to a safe default
|
|
199
216
|
const effectiveSelectedKind = selectedKind ?? { name: 'pods', kind: 'Pod', group: '' }
|
|
200
217
|
|
|
@@ -335,6 +352,69 @@ export function ResourcesSidebar({
|
|
|
335
352
|
})
|
|
336
353
|
}
|
|
337
354
|
|
|
355
|
+
// --- Keyboard navigation ---
|
|
356
|
+
// Flat list of all navigable kinds in the order they appear in the sidebar.
|
|
357
|
+
const flatVisibleKinds = useMemo<SelectedKindInfo[]>(() => {
|
|
358
|
+
const kinds: SelectedKindInfo[] = []
|
|
359
|
+
if (favoritesExpanded) {
|
|
360
|
+
for (const p of pinned) {
|
|
361
|
+
kinds.push({ name: p.name, kind: p.kind, group: p.group })
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
if (filteredCategories) {
|
|
365
|
+
for (const cat of filteredCategories) {
|
|
366
|
+
if (effectiveExpandedCategories.has(cat.name)) {
|
|
367
|
+
for (const r of cat.visibleResources) {
|
|
368
|
+
kinds.push({ name: r.name, kind: r.kind, group: r.group })
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
return kinds
|
|
374
|
+
}, [favoritesExpanded, pinned, filteredCategories, effectiveExpandedCategories])
|
|
375
|
+
|
|
376
|
+
const [highlightedIndex, setHighlightedIndex] = useState(-1)
|
|
377
|
+
// Reset highlight when the filter or kind list changes
|
|
378
|
+
useEffect(() => {
|
|
379
|
+
setHighlightedIndex(kindFilter ? 0 : -1)
|
|
380
|
+
}, [kindFilter]) // eslint-disable-line react-hooks/exhaustive-deps
|
|
381
|
+
|
|
382
|
+
const highlightedKind = highlightedIndex >= 0 && highlightedIndex < flatVisibleKinds.length
|
|
383
|
+
? flatVisibleKinds[highlightedIndex]
|
|
384
|
+
: null
|
|
385
|
+
|
|
386
|
+
// Scroll the highlighted kind button into view
|
|
387
|
+
const highlightedRef = useRef<HTMLButtonElement>(null)
|
|
388
|
+
useEffect(() => {
|
|
389
|
+
if (highlightedRef.current) {
|
|
390
|
+
highlightedRef.current.scrollIntoView({ block: 'nearest', behavior: 'smooth' })
|
|
391
|
+
}
|
|
392
|
+
}, [highlightedIndex])
|
|
393
|
+
|
|
394
|
+
const handleSearchKeyDown = useCallback((e: React.KeyboardEvent<HTMLInputElement>) => {
|
|
395
|
+
if (e.key === 'Escape') {
|
|
396
|
+
setKindFilter('')
|
|
397
|
+
setHighlightedIndex(-1)
|
|
398
|
+
;(e.target as HTMLInputElement).blur()
|
|
399
|
+
} else if (e.key === 'ArrowDown') {
|
|
400
|
+
e.preventDefault()
|
|
401
|
+
setHighlightedIndex(prev => Math.min(prev + 1, flatVisibleKinds.length - 1))
|
|
402
|
+
} else if (e.key === 'ArrowUp') {
|
|
403
|
+
e.preventDefault()
|
|
404
|
+
setHighlightedIndex(prev => Math.max(prev - 1, 0))
|
|
405
|
+
} else if (e.key === 'Enter' && highlightedKind) {
|
|
406
|
+
e.preventDefault()
|
|
407
|
+
selectKind(highlightedKind)
|
|
408
|
+
setHighlightedIndex(-1)
|
|
409
|
+
setKindFilter('')
|
|
410
|
+
onKindNavigated?.()
|
|
411
|
+
}
|
|
412
|
+
}, [flatVisibleKinds.length, highlightedKind, selectKind, onKindNavigated]) // eslint-disable-line react-hooks/exhaustive-deps
|
|
413
|
+
|
|
414
|
+
const isKindHighlighted = useCallback((name: string, group: string) => {
|
|
415
|
+
return highlightedKind?.name === name && highlightedKind?.group === group
|
|
416
|
+
}, [highlightedKind])
|
|
417
|
+
|
|
338
418
|
// Scroll sidebar to show selected kind on mount (deep linking) and on kind changes (keyboard nav)
|
|
339
419
|
const lastScrolledKind = useRef<string | null>(null)
|
|
340
420
|
const isInitialScroll = useRef(true)
|
|
@@ -362,11 +442,12 @@ export function ResourcesSidebar({
|
|
|
362
442
|
<div className="relative">
|
|
363
443
|
<Search className="absolute left-2 top-1/2 -translate-y-1/2 w-3.5 h-3.5 text-theme-text-tertiary" />
|
|
364
444
|
<input
|
|
445
|
+
ref={kindSearchRef}
|
|
365
446
|
type="text"
|
|
366
447
|
placeholder="Filter resources..."
|
|
367
448
|
value={kindFilter}
|
|
368
449
|
onChange={(e) => setKindFilter(e.target.value)}
|
|
369
|
-
onKeyDown={
|
|
450
|
+
onKeyDown={handleSearchKeyDown}
|
|
370
451
|
className="w-full pl-7 pr-7 py-2 bg-theme-elevated border border-theme-border-light rounded-lg text-sm text-theme-text-primary placeholder-theme-text-disabled focus:outline-none focus:ring-2 focus:ring-skyhook-500"
|
|
371
452
|
/>
|
|
372
453
|
{kindFilter && (
|
|
@@ -398,34 +479,41 @@ export function ResourcesSidebar({
|
|
|
398
479
|
</span>
|
|
399
480
|
)}
|
|
400
481
|
</button>
|
|
401
|
-
{
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
482
|
+
<div className={clsx(
|
|
483
|
+
'grid transition-[grid-template-rows] duration-200',
|
|
484
|
+
favoritesExpanded ? 'grid-rows-[1fr]' : 'grid-rows-[0fr]'
|
|
485
|
+
)} style={{ transitionTimingFunction: 'cubic-bezier(0.16, 1, 0.3, 1)' }}>
|
|
486
|
+
<div className="overflow-hidden">
|
|
487
|
+
<div className="space-y-0.5">
|
|
488
|
+
{pinned.length === 0 ? (
|
|
489
|
+
<div className="px-3 py-2 text-xs text-theme-text-disabled">
|
|
490
|
+
No pinned resources. Click <Pin className="w-3 h-3 inline" /> on any resource type to pin it here.
|
|
491
|
+
</div>
|
|
492
|
+
) : (
|
|
493
|
+
pinned.map((p) => {
|
|
494
|
+
const isResourceSelected =
|
|
495
|
+
(effectiveSelectedKind.name === p.name && effectiveSelectedKind.group === p.group) ||
|
|
496
|
+
(effectiveSelectedKind.kind.toLowerCase() === p.kind.toLowerCase() && effectiveSelectedKind.group === p.group)
|
|
497
|
+
const highlighted = isKindHighlighted(p.name, p.group)
|
|
498
|
+
return (
|
|
499
|
+
<ResourceTypeButton
|
|
500
|
+
key={`${p.name}-${p.group}`}
|
|
501
|
+
ref={highlighted ? highlightedRef : (isResourceSelected ? selectedSidebarRef : null)}
|
|
502
|
+
resource={{ name: p.name, kind: p.kind, group: p.group, version: '', namespaced: true, isCrd: false, verbs: [] }}
|
|
503
|
+
count={counts?.[(p.group ? `${p.group}/${p.kind}` : p.kind)] ?? 0}
|
|
504
|
+
isSelected={isResourceSelected}
|
|
505
|
+
isHighlighted={highlighted}
|
|
506
|
+
isForbidden={forbiddenKinds.has(p.group ? `${p.group}/${p.kind}` : p.kind)}
|
|
507
|
+
isPinned={true}
|
|
508
|
+
onTogglePin={() => togglePin(p)}
|
|
509
|
+
onClick={() => selectKind({ name: p.name, kind: p.kind, group: p.group })}
|
|
510
|
+
/>
|
|
511
|
+
)
|
|
512
|
+
})
|
|
513
|
+
)}
|
|
514
|
+
</div>
|
|
427
515
|
</div>
|
|
428
|
-
|
|
516
|
+
</div>
|
|
429
517
|
</div>
|
|
430
518
|
{filteredCategories ? (
|
|
431
519
|
// Dynamic categories from API
|
|
@@ -449,31 +537,38 @@ export function ResourcesSidebar({
|
|
|
449
537
|
</span>
|
|
450
538
|
)}
|
|
451
539
|
</button>
|
|
452
|
-
{
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
540
|
+
<div className={clsx(
|
|
541
|
+
'grid transition-[grid-template-rows] duration-200',
|
|
542
|
+
isExpanded ? 'grid-rows-[1fr]' : 'grid-rows-[0fr]'
|
|
543
|
+
)} style={{ transitionTimingFunction: 'cubic-bezier(0.16, 1, 0.3, 1)' }}>
|
|
544
|
+
<div className="overflow-hidden">
|
|
545
|
+
<div className="space-y-0.5">
|
|
546
|
+
{category.visibleResources.map((resource) => {
|
|
547
|
+
const resourceIsPinned = isPinned(resource.name, resource.group)
|
|
548
|
+
const isResourceSelected =
|
|
549
|
+
(effectiveSelectedKind.name === resource.name && effectiveSelectedKind.group === resource.group) ||
|
|
550
|
+
(effectiveSelectedKind.kind.toLowerCase() === resource.kind.toLowerCase() && effectiveSelectedKind.group === resource.group)
|
|
551
|
+
// If the resource is pinned, let the Favorites section own the highlight
|
|
552
|
+
const showSelected = isResourceSelected && !resourceIsPinned
|
|
553
|
+
const highlighted = isKindHighlighted(resource.name, resource.group)
|
|
554
|
+
return (
|
|
555
|
+
<ResourceTypeButton
|
|
556
|
+
key={resource.name}
|
|
557
|
+
ref={highlighted ? highlightedRef : (isResourceSelected ? selectedSidebarRef : null)}
|
|
558
|
+
resource={resource}
|
|
559
|
+
count={counts?.[(resource.group ? `${resource.group}/${resource.kind}` : resource.kind)] ?? 0}
|
|
560
|
+
isSelected={showSelected}
|
|
561
|
+
isHighlighted={highlighted}
|
|
562
|
+
isForbidden={forbiddenKinds.has(resource.group ? `${resource.group}/${resource.kind}` : resource.kind)}
|
|
563
|
+
isPinned={resourceIsPinned}
|
|
564
|
+
onTogglePin={() => togglePin({ name: resource.name, kind: resource.kind, group: resource.group })}
|
|
565
|
+
onClick={() => selectKind({ name: resource.name, kind: resource.kind, group: resource.group })}
|
|
566
|
+
/>
|
|
567
|
+
)
|
|
568
|
+
})}
|
|
569
|
+
</div>
|
|
475
570
|
</div>
|
|
476
|
-
|
|
571
|
+
</div>
|
|
477
572
|
</div>
|
|
478
573
|
)
|
|
479
574
|
})
|
|
@@ -2938,6 +2938,11 @@ export function ResourcesView({
|
|
|
2938
2938
|
pinned={pinned}
|
|
2939
2939
|
togglePin={togglePin}
|
|
2940
2940
|
isPinned={isPinned}
|
|
2941
|
+
onKindNavigated={() => {
|
|
2942
|
+
// After selecting a kind via keyboard, move focus to the table search
|
|
2943
|
+
// so the user can immediately filter within the selected kind.
|
|
2944
|
+
setTimeout(() => searchInputRef.current?.focus(), 50)
|
|
2945
|
+
}}
|
|
2941
2946
|
/>
|
|
2942
2947
|
)}
|
|
2943
2948
|
|
|
@@ -2953,6 +2958,30 @@ export function ResourcesView({
|
|
|
2953
2958
|
placeholder="Search... (press /)"
|
|
2954
2959
|
value={searchTerm}
|
|
2955
2960
|
onChange={(e) => setSearchTerm(e.target.value)}
|
|
2961
|
+
onKeyDown={(e) => {
|
|
2962
|
+
if (e.key === 'ArrowDown') {
|
|
2963
|
+
// Hand off to the table's keyboard navigation — blur the input
|
|
2964
|
+
// so the registered ArrowDown/j/k shortcuts take over, and
|
|
2965
|
+
// highlight the first row.
|
|
2966
|
+
e.preventDefault()
|
|
2967
|
+
searchInputRef.current?.blur()
|
|
2968
|
+
setHighlightedIndex(0)
|
|
2969
|
+
} else if (e.key === 'Enter' && filteredResourceCountRef.current > 0) {
|
|
2970
|
+
// Select the first (or currently highlighted) resource
|
|
2971
|
+
e.preventDefault()
|
|
2972
|
+
searchInputRef.current?.blur()
|
|
2973
|
+
if (highlightedIndex < 0) setHighlightedIndex(0)
|
|
2974
|
+
// Defer to next frame so the highlight renders before we open
|
|
2975
|
+
requestAnimationFrame(() => {
|
|
2976
|
+
const res = highlightedResourceRef.current ?? filteredResources[0]
|
|
2977
|
+
if (res?.metadata?.name) {
|
|
2978
|
+
onResourceClick?.({ kind: selectedKind.name, namespace: res.metadata.namespace || '', name: res.metadata.name, group: selectedKind.group })
|
|
2979
|
+
}
|
|
2980
|
+
})
|
|
2981
|
+
} else if (e.key === 'Escape') {
|
|
2982
|
+
searchInputRef.current?.blur()
|
|
2983
|
+
}
|
|
2984
|
+
}}
|
|
2956
2985
|
className="w-full max-w-md pl-10 pr-4 py-2 bg-theme-elevated border border-theme-border-light rounded-lg text-sm text-theme-text-primary placeholder-theme-text-disabled focus:outline-none focus:ring-2 focus:ring-skyhook-500"
|
|
2957
2986
|
/>
|
|
2958
2987
|
</div>
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useState, useCallback } from 'react'
|
|
1
|
+
import { useState, useCallback, useEffect, useRef } from 'react'
|
|
2
2
|
import {
|
|
3
3
|
Copy,
|
|
4
4
|
CopyPlus,
|
|
@@ -91,6 +91,18 @@ function formatSaveError(error: string): { summary: string; details?: string } {
|
|
|
91
91
|
return { summary: error }
|
|
92
92
|
}
|
|
93
93
|
|
|
94
|
+
// Safe sessionStorage wrappers — storage can throw QuotaExceededError or be
|
|
95
|
+
// blocked by browser security policies. Draft persistence is best-effort.
|
|
96
|
+
function safeSessionGet(key: string): string | null {
|
|
97
|
+
try { return sessionStorage.getItem(key) } catch { return null }
|
|
98
|
+
}
|
|
99
|
+
function safeSessionSet(key: string, value: string): void {
|
|
100
|
+
try { sessionStorage.setItem(key, value) } catch { /* best-effort */ }
|
|
101
|
+
}
|
|
102
|
+
function safeSessionRemove(key: string): void {
|
|
103
|
+
try { sessionStorage.removeItem(key) } catch { /* best-effort */ }
|
|
104
|
+
}
|
|
105
|
+
|
|
94
106
|
interface EditableYamlViewProps {
|
|
95
107
|
resource: SelectedResource
|
|
96
108
|
data: any
|
|
@@ -109,11 +121,34 @@ interface EditableYamlViewProps {
|
|
|
109
121
|
}
|
|
110
122
|
|
|
111
123
|
export function EditableYamlView({ resource, data, onCopy, copied, onSaved, onSave, isSaving, saveError, onDuplicate }: EditableYamlViewProps) {
|
|
112
|
-
const
|
|
113
|
-
|
|
124
|
+
const draftKey = `radar_yaml_draft:${resource.kind}/${resource.namespace}/${resource.name}`
|
|
125
|
+
|
|
126
|
+
// Restore draft from sessionStorage (e.g., after session-expiry redirect).
|
|
127
|
+
// All sessionStorage calls are wrapped in try-catch — storage can throw
|
|
128
|
+
// QuotaExceededError or be blocked by browser security policies.
|
|
129
|
+
const savedDraft = useRef(safeSessionGet(draftKey))
|
|
130
|
+
const [isEditing, setIsEditing] = useState(savedDraft.current !== null)
|
|
131
|
+
const [editedYaml, setEditedYaml] = useState(savedDraft.current ?? '')
|
|
114
132
|
const [yamlErrors, setYamlErrors] = useState<string[]>([])
|
|
115
133
|
const [showErrorDetails, setShowErrorDetails] = useState(false)
|
|
116
134
|
|
|
135
|
+
// Clean up restored draft flag
|
|
136
|
+
useEffect(() => {
|
|
137
|
+
if (typeof savedDraft.current === 'string') {
|
|
138
|
+
safeSessionRemove(draftKey)
|
|
139
|
+
savedDraft.current = null
|
|
140
|
+
}
|
|
141
|
+
}, [draftKey])
|
|
142
|
+
|
|
143
|
+
// Autosave draft to sessionStorage while editing (best-effort)
|
|
144
|
+
useEffect(() => {
|
|
145
|
+
if (isEditing && editedYaml) {
|
|
146
|
+
safeSessionSet(draftKey, editedYaml)
|
|
147
|
+
} else {
|
|
148
|
+
safeSessionRemove(draftKey)
|
|
149
|
+
}
|
|
150
|
+
}, [isEditing, editedYaml, draftKey])
|
|
151
|
+
|
|
117
152
|
// Convert resource to YAML for editing
|
|
118
153
|
const convertToYaml = useCallback((d: any) => {
|
|
119
154
|
if (!d) return ''
|
|
@@ -109,6 +109,22 @@ export function Tooltip({
|
|
|
109
109
|
}
|
|
110
110
|
}, [])
|
|
111
111
|
|
|
112
|
+
// When disabled flips true, proactively cancel any pending show timer and
|
|
113
|
+
// clear visible state. Without this, a tooltip that was visible (or armed)
|
|
114
|
+
// when disabled became true would pop back on as soon as disabled flips
|
|
115
|
+
// false — even though the cursor is elsewhere and no fresh mouseenter has
|
|
116
|
+
// fired. Also covers the case where the trigger becomes unreachable via
|
|
117
|
+
// pointer-events-none and never fires mouseleave.
|
|
118
|
+
useEffect(() => {
|
|
119
|
+
if (disabled) {
|
|
120
|
+
if (timeoutRef.current) {
|
|
121
|
+
clearTimeout(timeoutRef.current)
|
|
122
|
+
timeoutRef.current = null
|
|
123
|
+
}
|
|
124
|
+
setIsVisible(false)
|
|
125
|
+
}
|
|
126
|
+
}, [disabled])
|
|
127
|
+
|
|
112
128
|
if (disabled || !content) {
|
|
113
129
|
return <>{children}</>
|
|
114
130
|
}
|