Fix text overflowing template dropdown trigger, add new MultilineInput component with popover for editing, remove MultiInputCell component except for code to create new MultiSelectCell component

This commit is contained in:
2025-03-14 00:44:44 -04:00
parent 0f89373d11
commit 0ef27a3229
7 changed files with 383 additions and 332 deletions

View File

@@ -172,7 +172,19 @@ const SearchableTemplateSelect: React.FC<SearchableTemplateSelectProps> = ({
if (!value) return placeholder;
const template = templates.find(t => t.id.toString() === value);
if (!template) return placeholder;
return getTemplateDisplayText(value);
// Get the original display text
const originalText = getTemplateDisplayText(value);
// Check if it has the expected format "Brand - Product Type"
if (originalText.includes(' - ')) {
const [brand, productType] = originalText.split(' - ', 2);
// Reverse the order to "Product Type - Brand"
return `${productType} - ${brand}`;
}
// If it doesn't match the expected format, return the original text
return originalText;
} catch (err) {
console.error('Error getting display text:', err);
return placeholder;
@@ -218,10 +230,10 @@ const SearchableTemplateSelect: React.FC<SearchableTemplateSelectProps> = ({
variant="outline"
role="combobox"
aria-expanded={open}
className={cn("w-full justify-between", triggerClassName)}
className={cn("w-full justify-between overflow-hidden", triggerClassName)}
>
{getDisplayText()}
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
<span className="truncate overflow-hidden mr-2">{getDisplayText()}</span>
<ChevronsUpDown className="h-4 w-4 shrink-0 opacity-50 flex-none" />
</Button>
</PopoverTrigger>
<PopoverContent className={cn("w-[300px] p-0", className)}>

View File

@@ -9,7 +9,7 @@ import {
} from '@/components/ui/tooltip'
import InputCell from './cells/InputCell'
import SelectCell from './cells/SelectCell'
import MultiInputCell from './cells/MultiInputCell'
import MultiSelectCell from './cells/MultiSelectCell'
import { TableCell } from '@/components/ui/table'
// Define error object type
@@ -88,7 +88,7 @@ const BaseCellContent = React.memo(({
if (fieldType === 'multi-select' || fieldType === 'multi-input') {
return (
<MultiInputCell
<MultiSelectCell
field={field}
value={value}
onChange={onChange}

View File

@@ -67,9 +67,9 @@ const MemoizedTemplateSelect = React.memo(({
}) => {
if (isLoading) {
return (
<Button variant="outline" className="w-full justify-between" disabled>
<Loader2 className="h-4 w-4 mr-2 animate-spin" />
Loading...
<Button variant="outline" className="w-full justify-between overflow-hidden" disabled>
<Loader2 className="h-4 w-4 mr-2 animate-spin flex-none" />
<span className="truncate overflow-hidden">Loading...</span>
</Button>
);
}
@@ -217,15 +217,17 @@ const ValidationTable = <T extends string>({
const rowIndex = data.findIndex(r => r === row.original);
return (
<TableCell className="p-1" style={{ width: '200px', minWidth: '200px', maxWidth: '200px' }}>
<MemoizedTemplateSelect
templates={templates}
value={templateValue || ''}
onValueChange={(value) => handleTemplateChange(value, rowIndex)}
getTemplateDisplayText={getTemplateDisplayText}
defaultBrand={defaultBrand}
isLoading={isLoadingTemplates}
/>
<TableCell className="p-1" style={{ width: '200px', minWidth: '200px', maxWidth: '200px', overflow: 'hidden' }}>
<div className="w-full overflow-hidden">
<MemoizedTemplateSelect
templates={templates}
value={templateValue || ''}
onValueChange={(value) => handleTemplateChange(value, rowIndex)}
getTemplateDisplayText={getTemplateDisplayText}
defaultBrand={defaultBrand}
isLoading={isLoadingTemplates}
/>
</div>
</TableCell>
);
}

View File

@@ -1,8 +1,8 @@
import React, { useState, useCallback, useDeferredValue, useTransition, useRef, useEffect } from 'react'
import React, { useState, useCallback, useDeferredValue, useTransition, useRef, useEffect, useMemo } from 'react'
import { Field } from '../../../../types'
import { Input } from '@/components/ui/input'
import { Textarea } from '@/components/ui/textarea'
import { cn } from '@/lib/utils'
import MultilineInput from './MultilineInput'
interface InputCellProps<T extends string> {
field: Field<T>
@@ -31,6 +31,7 @@ const formatPrice = (value: string): string => {
};
const InputCell = <T extends string>({
field,
value,
onChange,
onStartEdit,
@@ -48,6 +49,18 @@ const InputCell = <T extends string>({
// Use a ref to track if we need to process the value
const needsProcessingRef = useRef(false);
// Track local display value to avoid waiting for validation
const [localDisplayValue, setLocalDisplayValue] = useState<string | null>(null);
// Initialize localDisplayValue on mount and when value changes externally
useEffect(() => {
if (localDisplayValue === null ||
(typeof value === 'string' && typeof localDisplayValue === 'string' &&
value.trim() !== localDisplayValue.trim())) {
setLocalDisplayValue(value);
}
}, [value, localDisplayValue]);
// Efficiently handle price formatting without multiple rerenders
useEffect(() => {
if (isPrice && needsProcessingRef.current && !isEditing) {
@@ -93,6 +106,9 @@ const InputCell = <T extends string>({
needsProcessingRef.current = true;
}
// Update local display value immediately
setLocalDisplayValue(processedValue);
onChange(processedValue);
onEndEdit?.();
});
@@ -104,14 +120,30 @@ const InputCell = <T extends string>({
setEditValue(newValue);
}, [isPrice]);
// Display value with efficient memoization
const displayValue = useDeferredValue(
isPrice && value ?
typeof value === 'number' ? value.toFixed(2) :
typeof value === 'string' && /^-?\d+(\.\d+)?$/.test(value) ? parseFloat(value).toFixed(2) :
value :
value ?? ''
);
// Get the display value - prioritize local display value
const displayValue = useMemo(() => {
// First priority: local display value (for immediate updates)
if (localDisplayValue !== null) {
if (isPrice) {
// Format price value
const numValue = parseFloat(localDisplayValue);
return !isNaN(numValue) ? numValue.toFixed(2) : localDisplayValue;
}
return localDisplayValue;
}
// Second priority: handle price formatting for the actual value
if (isPrice && value) {
if (typeof value === 'number') {
return value.toFixed(2);
} else if (typeof value === 'string' && /^-?\d+(\.\d+)?$/.test(value)) {
return parseFloat(value).toFixed(2);
}
}
// Default: use the actual value or empty string
return value ?? '';
}, [isPrice, value, localDisplayValue]);
// Add outline even when not in focus
const outlineClass = "border focus-visible:ring-0 focus-visible:ring-offset-0";
@@ -128,48 +160,48 @@ const InputCell = <T extends string>({
</div>
);
}
// Render multiline fields using the dedicated MultilineInput component
if (isMultiline) {
return (
<MultilineInput
field={field}
value={value}
onChange={onChange}
hasErrors={hasErrors}
disabled={disabled}
/>
);
}
// Original component for non-multiline fields
return (
<div className="w-full">
{isMultiline ? (
<Textarea
value={isEditing ? editValue : (value ?? '')}
{isEditing ? (
<Input
type="text"
value={editValue}
onChange={handleChange}
onFocus={handleFocus}
onBlur={handleBlur}
autoFocus
className={cn(
"min-h-[80px] resize-none",
outlineClass,
hasErrors ? "border-destructive" : ""
hasErrors ? "border-destructive" : "",
isPending ? "opacity-50" : ""
)}
/>
) : (
isEditing ? (
<Input
type="text"
value={editValue}
onChange={handleChange}
onFocus={handleFocus}
onBlur={handleBlur}
autoFocus
className={cn(
outlineClass,
hasErrors ? "border-destructive" : "",
isPending ? "opacity-50" : ""
)}
/>
) : (
<div
onClick={handleFocus}
className={cn(
"px-3 py-2 h-10 rounded-md text-sm w-full cursor-text flex items-center",
outlineClass,
hasErrors ? "border-destructive" : "border-input"
)}
>
{displayValue}
</div>
)
<div
onClick={handleFocus}
className={cn(
"px-3 py-2 h-10 rounded-md text-sm w-full cursor-text flex items-center",
outlineClass,
hasErrors ? "border-destructive" : "border-input"
)}
>
{displayValue}
</div>
)}
</div>
)
@@ -181,6 +213,7 @@ export default React.memo(InputCell, (prev, next) => {
if (prev.isMultiline !== next.isMultiline) return false;
if (prev.isPrice !== next.isPrice) return false;
if (prev.disabled !== next.disabled) return false;
if (prev.field !== next.field) return false;
// Only check value if not editing (to avoid expensive rerender during editing)
if (prev.value !== next.value) {

View File

@@ -1,6 +1,5 @@
import React, { useState, useCallback, useMemo, useEffect, useRef, useLayoutEffect } from 'react'
import React, { useState, useCallback, useMemo, useEffect, useRef } from 'react'
import { Field } from '../../../../types'
import { Textarea } from '@/components/ui/textarea'
import { cn } from '@/lib/utils'
import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from '@/components/ui/command'
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover'
@@ -14,23 +13,17 @@ interface FieldOption {
value: string;
}
interface MultiInputCellProps<T extends string> {
interface MultiSelectCellProps<T extends string> {
field: Field<T>
value: string[]
onChange: (value: string[]) => void
onStartEdit?: () => void
onEndEdit?: () => void
hasErrors?: boolean
separator?: string
isMultiline?: boolean
isPrice?: boolean
options?: readonly FieldOption[]
disabled?: boolean
}
// Add global CSS to ensure fixed width constraints - use !important to override other styles
// Memoized option item to prevent unnecessary renders for large option lists
const OptionItem = React.memo(({
option,
@@ -154,19 +147,16 @@ const VirtualizedOptions = React.memo(({
VirtualizedOptions.displayName = 'VirtualizedOptions';
const MultiInputCell = <T extends string>({
const MultiSelectCell = <T extends string>({
field,
value = [],
onChange,
onStartEdit,
onEndEdit,
hasErrors,
separator = ',',
isMultiline = false,
isPrice = false,
options: providedOptions,
disabled = false
}: MultiInputCellProps<T>) => {
}: MultiSelectCellProps<T>) => {
const [open, setOpen] = useState(false)
const [searchQuery, setSearchQuery] = useState("")
// Add internal state for tracking selections
@@ -186,8 +176,6 @@ const MultiInputCell = <T extends string>({
// Handle open state changes with improved responsiveness
const handleOpenChange = useCallback((newOpen: boolean) => {
setOpen(newOpen);
if (open && !newOpen) {
// Only update parent state when dropdown closes
// Avoid expensive deep comparison if lengths are different
@@ -196,7 +184,7 @@ const MultiInputCell = <T extends string>({
onChange(internalValue);
}
if (onEndEdit) onEndEdit();
} else if (newOpen) {
} else if (newOpen && !open) {
// Sync internal state with external state when opening
setInternalValue(value);
setSearchQuery(""); // Reset search query on open
@@ -310,33 +298,6 @@ const MultiInputCell = <T extends string>({
});
}, []);
// Handle focus
const handleFocus = useCallback(() => {
if (onStartEdit) onStartEdit();
}, [onStartEdit]);
// Handle blur
const handleBlur = useCallback(() => {
if (onEndEdit) onEndEdit();
}, [onEndEdit]);
// Handle direct input changes
const handleInputChange = useCallback((e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
const newValue = e.target.value.split(separator).map(v => v.trim()).filter(Boolean);
setInternalValue(newValue);
onChange(newValue);
}, [separator, onChange]);
// Format display values for price
const getDisplayValues = useCallback(() => {
if (!isPrice) return selectedValues.map(v => v.label);
return selectedValues.map(v => {
const numValue = parseFloat(v.value.replace(/[^\d.]/g, ''))
return !isNaN(numValue) ? `$${numValue.toFixed(2)}` : v.value
});
}, [selectedValues, isPrice]);
// Handle wheel scroll in dropdown
const handleWheel = useCallback((e: React.WheelEvent) => {
if (commandListRef.current) {
@@ -345,9 +306,6 @@ const MultiInputCell = <T extends string>({
}
}, []);
// Add outline even when not in focus
const outlineClass = "border focus-visible:ring-0 focus-visible:ring-offset-0"
// If disabled, render a static view
if (disabled) {
// Handle array values
@@ -369,241 +327,93 @@ const MultiInputCell = <T extends string>({
);
}
// If we have a multi-select field with options, use command UI
if (field.fieldType.type === 'multi-select' && selectOptions.length > 0) {
return (
<Popover
open={open}
onOpenChange={(isOpen) => {
setOpen(isOpen);
handleOpenChange(isOpen);
}}
>
<PopoverTrigger asChild>
<Button
variant="outline"
role="combobox"
aria-expanded={open}
className={cn(
"w-full justify-between font-normal",
"border",
!internalValue.length && "text-muted-foreground",
hasErrors ? "border-destructive" : ""
)}
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
setOpen(!open);
if (!open && onStartEdit) onStartEdit();
}}
>
<div className="flex items-center w-full justify-between">
<div className="flex items-center gap-2 overflow-hidden">
{internalValue.length === 0 ? (
<span className="text-muted-foreground truncate w-full">Select...</span>
) : internalValue.length === 1 ? (
<span className="truncate w-full">{selectedValues[0].label}</span>
) : (
<>
<Badge variant="secondary" className="shrink-0 whitespace-nowrap">
{internalValue.length} selected
</Badge>
<span className="truncate">
{selectedValues.map(v => v.label).join(', ')}
</span>
</>
)}
</div>
<ChevronsUpDown className="mx-2 h-4 w-4 shrink-0 opacity-50" />
</div>
</Button>
</PopoverTrigger>
<PopoverContent
className="p-0 w-[var(--radix-popover-trigger-width)]"
align="start"
sideOffset={4}
>
<Command shouldFilter={false}>
<CommandInput
placeholder="Search..."
className="h-9"
value={searchQuery}
onValueChange={setSearchQuery}
/>
<CommandList
ref={commandListRef}
onWheel={handleWheel}
className="max-h-[200px]"
>
<CommandEmpty>No results found.</CommandEmpty>
<CommandGroup>
{sortedOptions.map((option) => (
<CommandItem
key={option.value}
value={option.label}
onSelect={() => handleSelect(option.value)}
className="cursor-pointer"
>
{option.label}
{selectedValueSet.has(option.value) && (
<Check className="ml-auto h-4 w-4" />
)}
</CommandItem>
))}
</CommandGroup>
</CommandList>
</Command>
</PopoverContent>
</Popover>
);
}
// For standard multi-input without options, use text input
// Ensure the non-dropdown version also respects the width constraints
if (field.width) {
const cellWidth = field.width;
// Create a reference to the container element
const containerRef = useRef<HTMLDivElement>(null);
// Create a key-value map for inline styles with fixed width - simplified
// Use layout effect more efficiently - only for the button element
// since the container already uses inline styles
useLayoutEffect(() => {
// Skip if no width specified
if (!cellWidth) return;
// Cache previous width to avoid unnecessary DOM updates
const prevWidth = containerRef.current?.getAttribute('data-prev-width');
// Only update if width changed
if (prevWidth !== String(cellWidth) && containerRef.current) {
// Store new width for next comparison
containerRef.current.setAttribute('data-prev-width', String(cellWidth));
// Only manipulate the button element directly since we can't
// reliably style it with CSS in all cases
const button = containerRef.current.querySelector('button');
if (button) {
const htmlButton = button as HTMLElement;
htmlButton.style.width = `${cellWidth}px`;
htmlButton.style.minWidth = `${cellWidth}px`;
htmlButton.style.maxWidth = `${cellWidth}px`;
htmlButton.style.boxSizing = 'border-box';
}
}
}, [cellWidth]);
return (
<div
ref={containerRef}
className="inline-block fixed-width-cell"
style={{
width: `${cellWidth}px`,
minWidth: `${cellWidth}px`,
maxWidth: `${cellWidth}px`,
boxSizing: 'border-box',
}}
data-width={cellWidth}
>
{isMultiline ? (
<Textarea
value={internalValue.join(separator)}
onChange={handleInputChange}
onFocus={handleFocus}
onBlur={handleBlur}
placeholder={`Enter values separated by ${separator}`}
className={cn(
"min-h-[80px] resize-none",
outlineClass,
hasErrors ? "border-destructive" : ""
)}
style={{
width: `${cellWidth}px`,
minWidth: `${cellWidth}px`,
maxWidth: `${cellWidth}px`,
boxSizing: 'border-box',
}}
/>
) : (
<div
className={cn(
"flex h-9 rounded-md border px-3 py-1 text-sm",
"cursor-text truncate",
outlineClass,
hasErrors ? "border-destructive" : "",
"overflow-hidden items-center",
"w-full"
)}
onClick={handleFocus}
style={{
width: `${cellWidth}px`,
minWidth: `${cellWidth}px`,
maxWidth: `${cellWidth}px`,
boxSizing: 'border-box',
}}
>
{internalValue.length > 0 ? getDisplayValues().join(`, `) : (
<span className="text-muted-foreground truncate">
{`Enter values separated by ${separator}`}
</span>
)}
</div>
)}
</div>
);
}
// Fallback to default behavior if no width is specified
return (
<div className="w-full overflow-hidden" style={{ boxSizing: 'border-box' }}>
{isMultiline ? (
<Textarea
value={internalValue.join(separator)}
onChange={handleInputChange}
onFocus={handleFocus}
onBlur={handleBlur}
placeholder={`Enter values separated by ${separator}`}
<Popover
open={open}
onOpenChange={(isOpen) => {
setOpen(isOpen);
handleOpenChange(isOpen);
}}
>
<PopoverTrigger asChild>
<Button
variant="outline"
role="combobox"
aria-expanded={open}
className={cn(
"min-h-[80px] resize-none w-full",
outlineClass,
"w-full justify-between font-normal",
"border",
!internalValue.length && "text-muted-foreground",
hasErrors ? "border-destructive" : ""
)}
style={{ boxSizing: 'border-box' }}
/>
) : (
<div
className={cn(
"flex h-9 w-full rounded-md border px-3 py-1 text-sm",
"cursor-text truncate", // Add truncate for text overflow
outlineClass,
hasErrors ? "border-destructive" : "",
"overflow-hidden items-center"
)}
onClick={handleFocus}
style={{ boxSizing: 'border-box' }}
>
{internalValue.length > 0 ? getDisplayValues().join(`, `) : (
<span className="text-muted-foreground truncate w-full">
{`Enter values separated by ${separator}`}
</span>
)}
</div>
)}
</div>
<div className="flex items-center w-full justify-between">
<div className="flex items-center gap-2 overflow-hidden">
{internalValue.length === 0 ? (
<span className="text-muted-foreground truncate w-full">Select...</span>
) : internalValue.length === 1 ? (
<span className="truncate w-full">{selectedValues[0].label}</span>
) : (
<>
<Badge variant="secondary" className="shrink-0 whitespace-nowrap">
{internalValue.length} selected
</Badge>
<span className="truncate">
{selectedValues.map(v => v.label).join(', ')}
</span>
</>
)}
</div>
<ChevronsUpDown className="mx-2 h-4 w-4 shrink-0 opacity-50" />
</div>
</Button>
</PopoverTrigger>
<PopoverContent
className="p-0 w-[var(--radix-popover-trigger-width)]"
align="start"
sideOffset={4}
>
<Command shouldFilter={false}>
<CommandInput
placeholder="Search..."
className="h-9"
value={searchQuery}
onValueChange={setSearchQuery}
/>
<CommandList
ref={commandListRef}
onWheel={handleWheel}
className="max-h-[200px]"
>
<CommandEmpty>No results found.</CommandEmpty>
<CommandGroup>
{sortedOptions.map((option) => (
<CommandItem
key={option.value}
value={option.label}
onSelect={() => handleSelect(option.value)}
className="cursor-pointer"
>
{option.label}
{selectedValueSet.has(option.value) && (
<Check className="ml-auto h-4 w-4" />
)}
</CommandItem>
))}
</CommandGroup>
</CommandList>
</Command>
</PopoverContent>
</Popover>
);
};
MultiInputCell.displayName = 'MultiInputCell';
MultiSelectCell.displayName = 'MultiSelectCell';
export default React.memo(MultiInputCell, (prev, next) => {
export default React.memo(MultiSelectCell, (prev, next) => {
// Quick check for reference equality of simple props
if (prev.hasErrors !== next.hasErrors ||
prev.disabled !== next.disabled ||
prev.isMultiline !== next.isMultiline ||
prev.isPrice !== next.isPrice ||
prev.separator !== next.separator) {
prev.disabled !== next.disabled) {
return false;
}

View File

@@ -0,0 +1,193 @@
import React, { useState, useCallback, useRef, useEffect } from 'react'
import { Field } from '../../../../types'
import { Textarea } from '@/components/ui/textarea'
import { cn } from '@/lib/utils'
import { Popover, PopoverTrigger, PopoverContent } from '@/components/ui/popover'
import { X } from 'lucide-react'
import { Button } from '@/components/ui/button'
interface MultilineInputProps<T extends string> {
field: Field<T>
value: any
onChange: (value: any) => void
hasErrors?: boolean
disabled?: boolean
}
const MultilineInput = <T extends string>({
field,
value,
onChange,
hasErrors = false,
disabled = false
}: MultilineInputProps<T>) => {
const [popoverOpen, setPopoverOpen] = useState(false);
const [editValue, setEditValue] = useState('');
const [localDisplayValue, setLocalDisplayValue] = useState<string | null>(null);
const cellRef = useRef<HTMLDivElement>(null);
const preventReopenRef = useRef(false);
const pendingChangeRef = useRef<string | null>(null);
// Initialize localDisplayValue on mount and when value changes externally
useEffect(() => {
if (localDisplayValue === null ||
(typeof value === 'string' && typeof localDisplayValue === 'string' &&
value.trim() !== localDisplayValue.trim())) {
setLocalDisplayValue(value);
}
}, [value, localDisplayValue]);
// Process any pending changes in the background
useEffect(() => {
if (pendingChangeRef.current !== null && !popoverOpen) {
const newValue = pendingChangeRef.current;
pendingChangeRef.current = null;
// Apply changes after the popover is closed
if (newValue !== value) {
onChange(newValue);
}
}
}, [popoverOpen, onChange, value]);
// Handle trigger click to toggle the popover
const handleTriggerClick = useCallback((e: React.MouseEvent) => {
if (preventReopenRef.current) {
e.preventDefault();
e.stopPropagation();
preventReopenRef.current = false;
return;
}
// Only process if not already open
if (!popoverOpen) {
setPopoverOpen(true);
// Initialize edit value from the current display
setEditValue(localDisplayValue || value || '');
}
}, [popoverOpen, value, localDisplayValue]);
// Handle immediate close of popover
const handleClosePopover = useCallback(() => {
// Only process if we have changes
if (editValue !== value || editValue !== localDisplayValue) {
// Store pending changes for async processing
pendingChangeRef.current = editValue;
// Update local display immediately
setLocalDisplayValue(editValue);
// Queue up the change to be processed in the background
setTimeout(() => {
onChange(editValue);
}, 0);
}
// Immediately close popover
setPopoverOpen(false);
// Prevent reopening
preventReopenRef.current = true;
setTimeout(() => {
preventReopenRef.current = false;
}, 100);
}, [editValue, value, localDisplayValue, onChange]);
// Handle clicking outside the popover
const handleInteractOutside = useCallback(() => {
handleClosePopover();
}, [handleClosePopover]);
// Handle popover open/close
const handlePopoverOpenChange = useCallback((open: boolean) => {
if (!open && popoverOpen) {
// Just call the close handler
handleClosePopover();
} else if (open && !popoverOpen) {
// When opening, set edit value from current display
setEditValue(localDisplayValue || value || '');
setPopoverOpen(true);
}
}, [value, popoverOpen, handleClosePopover, localDisplayValue]);
// Handle direct input change
const handleChange = useCallback((e: React.ChangeEvent<HTMLTextAreaElement>) => {
setEditValue(e.target.value);
}, []);
// Calculate display value
const displayValue = localDisplayValue !== null ? localDisplayValue : (value ?? '');
// Add outline even when not in focus
const outlineClass = "border focus-visible:ring-0 focus-visible:ring-offset-0";
// If disabled, just render the value without any interactivity
if (disabled) {
return (
<div className={cn(
"px-3 py-2 min-h-[80px] max-h-[80px] rounded-md text-sm w-full",
outlineClass,
hasErrors ? "border-destructive" : "border-input"
)}>
{displayValue}
</div>
);
}
return (
<div className="w-full" ref={cellRef}>
<Popover open={popoverOpen} onOpenChange={handlePopoverOpenChange}>
<PopoverTrigger asChild>
<div
onClick={handleTriggerClick}
className={cn(
"px-3 py-2 min-h-[80px] max-h-[80px] rounded-md text-sm w-full cursor-pointer",
"overflow-hidden whitespace-pre-wrap",
outlineClass,
hasErrors ? "border-destructive" : "border-input"
)}
>
{displayValue}
</div>
</PopoverTrigger>
<PopoverContent
className="p-0 shadow-lg rounded-md"
style={{ width: cellRef.current?.offsetWidth || 'auto' }}
align="start"
side="bottom"
alignOffset={0}
sideOffset={-80}
avoidCollisions={false}
onInteractOutside={handleInteractOutside}
forceMount
>
<div className="flex flex-col">
<Button
size="icon"
variant="ghost"
onClick={handleClosePopover}
className="h-6 w-6 text-muted-foreground absolute top-0.5 right-0.5"
>
<X className="h-3 w-3" />
</Button>
<Textarea
value={editValue}
onChange={handleChange}
className="min-h-[200px] border-none focus-visible:ring-0 rounded-none p-2"
placeholder={`Enter ${field.label || 'text'}...`}
autoFocus
/>
</div>
</PopoverContent>
</Popover>
</div>
);
};
export default React.memo(MultilineInput, (prev, next) => {
if (prev.hasErrors !== next.hasErrors) return false;
if (prev.disabled !== next.disabled) return false;
if (prev.field !== next.field) return false;
if (prev.value !== next.value) return false;
return true;
});