Skip to main content
Glama
EditableFieldLayout.tsx3.91 kB
'use client'; import { Check, Pencil, X } from 'lucide-react'; import { type FC, type HTMLAttributes, type ReactNode, useCallback, useEffect, useRef, useState, } from 'react'; import { cn } from '../../utils/cn'; import { Button, ButtonColor, ButtonSize, ButtonVariant } from '../Button'; type EditableFieldLayoutProps = { value?: string | null | undefined; onCancel: () => void; onSave: () => void; children: ReactNode; isDisabled?: boolean; } & HTMLAttributes<HTMLSpanElement>; export const EditableFieldLayout: FC<EditableFieldLayoutProps> = ({ value = '', onCancel, onSave, onClick, children, isDisabled, ...props }) => { const [isEditing, setIsEditing] = useState(false); const editableFieldRef = useRef<HTMLSpanElement>(null); const handleSave = (e: React.MouseEvent<HTMLButtonElement>) => { e.stopPropagation(); setIsEditing(false); onSave(); }; const handleCancel = (e: React.MouseEvent<HTMLButtonElement>) => { e.stopPropagation(); setIsEditing(false); onCancel(); }; const result = !value || value === '' ? '-' : value; const handleClickOutside = useCallback( (e: MouseEvent) => { // Check if there's any text selected const selection = window.getSelection(); if (selection && selection.toString().trim() !== '') { // If there is a selection, do not trigger the save and exit early. return; } // Proceed if the click is truly outside the component. if ( editableFieldRef.current && !editableFieldRef.current.contains(e.target as Node) ) { setIsEditing(false); onSave(); } }, [onSave] ); useEffect(() => { if (isEditing) { document.addEventListener('click', handleClickOutside); } else { document.removeEventListener('click', handleClickOutside); } return () => document.removeEventListener('click', handleClickOutside); }, [handleClickOutside, isEditing]); return ( <span className="group/editable-field flex gap-2" role="button" tabIndex={0} onClick={(e) => { setIsEditing(true); onClick?.(e); }} ref={editableFieldRef} {...props} > <div className={cn('flex flex-1 gap-2', isEditing ? 'display' : 'hidden')} key={String(isEditing)} > {children} <Button label="Save" variant={ButtonVariant.HOVERABLE} size={ButtonSize.ICON_SM} color={ButtonColor.TEXT} Icon={Check} onClick={handleSave} disabled={isDisabled} className="!text-current" role="button" tabIndex={0} aria-label="Save changes" data-testid="editable-field-save-button" /> <Button label="Cancel" color={ButtonColor.TEXT} variant={ButtonVariant.HOVERABLE} className="!text-current" size={ButtonSize.ICON_SM} Icon={X} onClick={handleCancel} disabled={isDisabled} role="button" tabIndex={0} aria-label="Cancel changes" data-testid="editable-field-cancel-button" /> </div> <div className={cn('flex flex-1 gap-2', isEditing ? 'hidden' : 'display')} > <span className="ml-2 flex w-full whitespace-pre-wrap p-1 text-sm leading-6"> {result} </span> <Button label="Edit" Icon={Pencil} color={ButtonColor.TEXT} variant={ButtonVariant.HOVERABLE} size={ButtonSize.ICON_SM} className="!text-current invisible group-hover/editable-field:visible" disabled={isDisabled} onClick={(e) => { e.stopPropagation(); setIsEditing(true); }} /> </div> </span> ); };

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/aymericzip/intlayer'

If you have feedback or need assistance with the MCP directory API, please join our Discord server