Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions src/app/content/ui/editable-error.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import {
Editable,
EditableError,
EditableInput,
EditablePreview,
} from "@/components/ui/editable";

export default function EditableErrorDemo() {
const error = "This field is required";

return (
<div className="w-96">
<Editable
defaultValue="Click to edit this text"
placeholder="Enter some text..."
hasError={Boolean(error)}
>
<EditablePreview className="w-80" />
<EditableInput className="w-80" aria-invalid={Boolean(error)} />
<EditableError>{error}</EditableError>
</Editable>
</div>
);
}
1 change: 1 addition & 0 deletions src/app/demo/[name]/ui/editable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,5 +23,6 @@ export const editable = {
},
components: {
Textarea: { component: "editable-textarea" },
"Editable with error": { component: "editable-error" },
},
};
78 changes: 65 additions & 13 deletions src/components/ui/editable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,8 @@ interface UseEditableProps {
selectAllOnFocus?: boolean;
/** Activation mode: 'click' or 'dblclick' */
activationMode?: ActivationMode;
/** Error message to display */
hasError?: boolean;
/** Callback when value is submitted */
onSubmit?: (value: string) => void;
/** Callback when value changes */
Expand Down Expand Up @@ -85,14 +87,17 @@ function useEditable(props: UseEditableProps = {}): UseEditableReturn {
startWithEditView = false,
selectAllOnFocus = true,
activationMode = "click",
hasError = false,
onSubmit,
onChange,
onValueChange,
onCancel,
onEdit,
} = props;

const [isEditing, setIsEditing] = React.useState(startWithEditView);
const [isEditing, setIsEditing] = React.useState(
startWithEditView || hasError,
);
const [internalValue, setInternalValue] = React.useState(defaultValue);
const [previousValue, setPreviousValue] = React.useState(defaultValue);
const inputRef = React.useRef<HTMLInputElement | HTMLTextAreaElement | null>(
Expand All @@ -114,14 +119,14 @@ function useEditable(props: UseEditableProps = {}): UseEditableReturn {
if (!isControlled) {
setInternalValue(previousValue);
}
setIsEditing(false);
setIsEditing(hasError || false);
onCancel?.(previousValue);
}, [isControlled, previousValue, onCancel]);
}, [isControlled, previousValue, onCancel, hasError]);

const submitEdit = React.useCallback(() => {
setIsEditing(false);
setIsEditing(hasError || false);
onSubmit?.(value);
}, [value, onSubmit]);
}, [value, onSubmit, hasError]);

const handleChange = React.useCallback(
(newValue: string) => {
Expand Down Expand Up @@ -169,7 +174,7 @@ function useEditable(props: UseEditableProps = {}): UseEditableReturn {

// Editable Root Component

const editableVariants = cva("inline-flex flex-col gap-1", {
const editableVariants = cva("inline-flex flex-col gap-1 relative", {
variants: {
size: {
sm: "text-sm",
Expand Down Expand Up @@ -203,6 +208,8 @@ interface EditableProps
selectAllOnFocus?: boolean;
/** Activation mode: 'click' or 'dblclick' */
activationMode?: ActivationMode;
/** Error message to display */
hasError?: boolean;
/** Callback when value is submitted */
onSubmit?: (value: string) => void;
/** Callback when value changes */
Expand All @@ -227,6 +234,7 @@ function Editable({
startWithEditView = false,
selectAllOnFocus = true,
activationMode = "click",
hasError = false,
onSubmit,
onChange,
onValueChange,
Expand All @@ -235,7 +243,9 @@ function Editable({
children,
...props
}: EditableProps) {
const [isEditing, setIsEditing] = React.useState(startWithEditView);
const [isEditing, setIsEditing] = React.useState(
startWithEditView || hasError,
);
const [internalValue, setInternalValue] = React.useState(defaultValue);
const [previousValue, setPreviousValue] = React.useState(defaultValue);
const inputRef = React.useRef<HTMLInputElement | HTMLTextAreaElement | null>(
Expand All @@ -257,14 +267,14 @@ function Editable({
if (!isControlled) {
setInternalValue(previousValue);
}
setIsEditing(false);
setIsEditing(hasError || false);
onCancel?.(previousValue);
}, [isControlled, previousValue, onCancel]);
}, [isControlled, previousValue, onCancel, hasError]);

const submitEdit = React.useCallback(() => {
setIsEditing(false);
setIsEditing(hasError || false);
onSubmit?.(value);
}, [value, onSubmit]);
}, [value, onSubmit, hasError]);

const handleChange = React.useCallback(
(newValue: string) => {
Expand Down Expand Up @@ -355,7 +365,7 @@ const editablePreviewVariants = cva(
[
"cursor-text rounded-md px-2 py-1 transition-colors",
"hover:bg-neutral-bg",
"min-h-[2rem] flex items-center whitespace-pre-line break-words",
"min-h-8 flex items-center whitespace-pre-line break-words",
].join(" "),
{
variants: {
Expand Down Expand Up @@ -474,7 +484,7 @@ function EditableInput({ className, ...props }: EditableInputProps) {
}
}}
className={cn(
"w-full border-2 bg-transparent dark:bg-transparent focus-visible:ring-0 focus-visible:border-primary transition-colors",
"w-full border-2 bg-transparent h-8 dark:bg-transparent focus-visible:ring-0 focus-visible:border-primary transition-colors",
className,
)}
{...props}
Expand Down Expand Up @@ -649,6 +659,47 @@ function EditableSubmitTrigger({
);
}

interface EditableErrorProps extends React.HTMLAttributes<HTMLDivElement> {
errors?: { message?: string }[];
}

function EditableError({
errors,
children,
className,
...props
}: EditableErrorProps) {
const errorMessages = errors?.filter(Boolean) || [];

if (errorMessages.length === 0 && !children) {
return null;
}

return (
<div
role="alert"
aria-live="polite"
data-slot="editable-error"
className={cn(
"text-sm text-destructive absolute w-max bg-white rounded-sm shadow-lg py-1 px-2 bottom-[calc(-100%+var(--spacing)*0.5)] cursor-default z-10",
className,
)}
{...props}
>
{children ||
(errorMessages.length === 1 ? (
<span>{errorMessages[0]?.message}</span>
) : (
<ul className="list-disc list-inside space-y-1">
{errorMessages.map((error, index) => (
<li key={index}>{error?.message}</li>
))}
</ul>
))}
</div>
);
}

export {
Editable,
EditableRootProvider,
Expand All @@ -659,6 +710,7 @@ export {
EditableEditTrigger,
EditableCancelTrigger,
EditableSubmitTrigger,
EditableError,
editableVariants,
editablePreviewVariants,
useEditable,
Expand Down
66 changes: 36 additions & 30 deletions src/lib/docsite/docsite-registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,7 @@ import CircularProgressDemo from "@/app/content/ui/circular-progress";
import CircularProgressWithTextDemo from "@/app/content/ui/circular-progress-text";
import CircularProgressVariantsDemo from "@/app/content/ui/circular-progress-variants";
import EditableDemo from "@/app/content/ui/editable";
import EditableErrorDemo from "@/app/content/ui/editable-error";
import EditableTextareaDemo from "@/app/content/ui/editable-textarea";
import FieldDemo from "@/app/content/ui/field";
import FieldCheckboxDemo from "@/app/content/ui/field-checkbox";
Expand All @@ -184,6 +185,11 @@ import FieldWithSeparatorDemo from "@/app/content/ui/field-separator";
import FieldSmallDemo from "@/app/content/ui/field-small";
import FieldSwitchDemo from "@/app/content/ui/field-switch";
import FieldTextareaDemo from "@/app/content/ui/field-textarea";
import FilterDemo from "@/app/content/ui/filter";
import FilterInputDemo from "@/app/content/ui/filter-input";
import FilterMultiSelectDemo from "@/app/content/ui/filter-multi-select";
import FilterSingleSelectDemo from "@/app/content/ui/filter-single-select";
import FilterToggleDemo from "@/app/content/ui/filter-toggle";
import InputGroupDemo from "@/app/content/ui/input-group";
import InputGroupDropdownDemo from "@/app/content/ui/input-group-dropdown";
import InputGroupSearchDemo from "@/app/content/ui/input-group-search";
Expand All @@ -202,11 +208,6 @@ import TimelineDemo from "@/app/content/ui/timeline";
import TimelineConnectorVariantsDemo from "@/app/content/ui/timeline-connector-variants";
import TimelineSizesDemo from "@/app/content/ui/timeline-sizes";
import TimelineVariantsDemo from "@/app/content/ui/timeline-variants";
import FilterDemo from "@/app/content/ui/filter";
import FilterInputDemo from "@/app/content/ui/filter-input";
import FilterSingleSelectDemo from "@/app/content/ui/filter-single-select";
import FilterMultiSelectDemo from "@/app/content/ui/filter-multi-select";
import FilterToggleDemo from "@/app/content/ui/filter-toggle";

export interface DocsiteRegistryEntry {
name: string;
Expand Down Expand Up @@ -606,6 +607,11 @@ export const docsiteRegistry: Record<string, DocsiteRegistryEntry> = {
path: "src/app/content/ui/editable-textarea.tsx",
component: EditableTextareaDemo,
},
"editable-error": {
name: "editable-error",
path: "src/app/content/ui/editable-error.tsx",
component: EditableErrorDemo,
},
"empty-states-no-results": {
name: "empty-states-no-results",
path: "src/app/content/ui/empty-states-no-search-results.tsx",
Expand Down Expand Up @@ -736,31 +742,31 @@ export const docsiteRegistry: Record<string, DocsiteRegistryEntry> = {
path: "src/app/content/ui/field-input-types.tsx",
component: FieldInputTypesDemo,
},
"filter": {
name: "filter",
path: "src/app/content/ui/filter.tsx",
component: FilterDemo,
},
"filter-input": {
name: "filter-input",
path: "src/app/content/ui/filter-input.tsx",
component: FilterInputDemo,
},
"filter-single-select": {
name: "filter-single-select",
path: "src/app/content/ui/filter-single-select.tsx",
component: FilterSingleSelectDemo,
},
"filter-multi-select": {
name: "filter-multi-select",
path: "src/app/content/ui/filter-multi-select.tsx",
component: FilterMultiSelectDemo,
},
"filter-toggle": {
name: "filter-toggle",
path: "src/app/content/ui/filter-toggle.tsx",
component: FilterToggleDemo,
},
filter: {
name: "filter",
path: "src/app/content/ui/filter.tsx",
component: FilterDemo,
},
"filter-input": {
name: "filter-input",
path: "src/app/content/ui/filter-input.tsx",
component: FilterInputDemo,
},
"filter-single-select": {
name: "filter-single-select",
path: "src/app/content/ui/filter-single-select.tsx",
component: FilterSingleSelectDemo,
},
"filter-multi-select": {
name: "filter-multi-select",
path: "src/app/content/ui/filter-multi-select.tsx",
component: FilterMultiSelectDemo,
},
"filter-toggle": {
name: "filter-toggle",
path: "src/app/content/ui/filter-toggle.tsx",
component: FilterToggleDemo,
},
icon: {
name: "icon",
path: "src/app/content/ui/icon-component.tsx",
Expand Down
5 changes: 4 additions & 1 deletion src/lib/right-sidebar-metadata.ts
Original file line number Diff line number Diff line change
Expand Up @@ -337,7 +337,10 @@ export const rightSidebarMetadata: Record<string, RightSidebarMetadata> = {
{
id: "examples",
title: "Examples",
children: [{ id: "editable-textarea", title: "Textarea" }],
children: [
{ id: "editable-textarea", title: "Textarea" },
{ id: "editable-error", title: "Editable with errors" },
],
},
],
},
Expand Down