feat(textarea): remove TextareaContext and update stories to reflect new error handling

This commit is contained in:
suyao
2025-12-01 20:28:46 +08:00
parent 3c89146458
commit 8ddf53d2d2
2 changed files with 29 additions and 55 deletions
@@ -1,28 +1,10 @@
import { cn } from '@cherrystudio/ui/utils/index'
import { composeEventHandlers } from '@radix-ui/primitive'
import { createContext } from '@radix-ui/react-context'
import { useCallbackRef } from '@radix-ui/react-use-callback-ref'
import { useControllableState } from '@radix-ui/react-use-controllable-state'
import { cva } from 'class-variance-authority'
import * as React from 'react'
/* -------------------------------------------------------------------------------------------------
* Textarea Context
* -----------------------------------------------------------------------------------------------*/
type TextareaContextValue = {
textareaId: string
hasError: boolean
disabled?: boolean
}
// eslint-disable-next-line @eslint-react/naming-convention/context-name
const [TextareaContext, useTextareaContext] = createContext<TextareaContextValue>('Textarea.TextareaContext', {
textareaId: '',
hasError: false,
disabled: false
})
/* -------------------------------------------------------------------------------------------------
* Variants
* -----------------------------------------------------------------------------------------------*/
@@ -55,21 +37,13 @@ const textareaVariants = cva(
const ROOT_NAME = 'TextareaRoot'
interface TextareaRootProps extends React.ComponentPropsWithoutRef<'div'> {
error?: string
disabled?: boolean
}
function TextareaRoot({ error, disabled, className, children, ...props }: TextareaRootProps) {
const textareaId = React.useId()
const hasError = !!error
interface TextareaRootProps extends React.ComponentPropsWithoutRef<'div'> {}
function TextareaRoot({ className, children, ...props }: TextareaRootProps) {
return (
<TextareaContext textareaId={textareaId} hasError={hasError} disabled={disabled}>
<div data-slot="textarea-root" {...props} className={cn('flex w-full flex-col gap-2', className)}>
{children}
</div>
</TextareaContext>
<div data-slot="textarea-root" {...props} className={cn('flex w-full flex-col gap-2', className)}>
{children}
</div>
)
}
@@ -85,18 +59,19 @@ interface TextareaInputProps extends Omit<React.ComponentPropsWithoutRef<'textar
value?: string
defaultValue?: string
onValueChange?: (value: string) => void
hasError?: boolean
ref?: React.Ref<HTMLTextAreaElement>
}
const TextareaInput = function TextareaInput({
ref,
function TextareaInput({
value: valueProp,
defaultValue,
onValueChange,
hasError = false,
className,
ref,
...props
}: TextareaInputProps & { ref?: React.RefObject<HTMLTextAreaElement | null> }) {
const context = useTextareaContext(INPUT_NAME)
}: TextareaInputProps) {
const [value = '', setValue] = useControllableState({
prop: valueProp,
defaultProp: defaultValue ?? '',
@@ -114,14 +89,12 @@ const TextareaInput = function TextareaInput({
return (
<textarea
data-slot="textarea-input"
id={context.textareaId}
{...props}
ref={ref}
value={value}
onChange={composeEventHandlers(props.onChange, handleChange)}
disabled={context.disabled}
aria-invalid={context.hasError}
className={cn(textareaVariants({ hasError: context.hasError }), className)}
aria-invalid={hasError}
className={cn(textareaVariants({ hasError }), className)}
/>
)
}
@@ -68,9 +68,9 @@ export const WithCaption: Story = {
// Error State
export const ErrorState: Story = {
render: () => (
<Textarea.Root error="This field cannot be empty" className="w-[400px]">
<Textarea.Root className="w-[400px]">
<div className="text-lg font-bold leading-[22px]">Message</div>
<Textarea.Input placeholder="Enter your message..." />
<Textarea.Input placeholder="Enter your message..." hasError />
<div className="text-sm flex items-center gap-1.5 leading-4 text-destructive">
<svg
xmlns="http://www.w3.org/2000/svg"
@@ -135,9 +135,9 @@ export const AutoResize: Story = {
// Disabled State
export const Disabled: Story = {
render: () => (
<Textarea.Root disabled className="w-[400px]">
<Textarea.Root className="w-[400px]">
<div className="text-lg font-bold leading-[22px] cursor-not-allowed opacity-70">Disabled Field</div>
<Textarea.Input defaultValue="This textarea is disabled" />
<Textarea.Input defaultValue="This textarea is disabled" disabled />
</Textarea.Root>
)
}
@@ -193,17 +193,17 @@ export const AllStates: Story = {
<div>
<p className="mb-2 text-sm font-semibold text-muted-foreground">Disabled State</p>
<Textarea.Root disabled className="w-[400px]">
<Textarea.Root className="w-[400px]">
<div className="text-lg font-bold leading-[22px] cursor-not-allowed opacity-70">Disabled</div>
<Textarea.Input defaultValue="Disabled textarea with content" />
<Textarea.Input defaultValue="Disabled textarea with content" disabled />
</Textarea.Root>
</div>
<div>
<p className="mb-2 text-sm font-semibold text-muted-foreground">Error State</p>
<Textarea.Root error="This field is required" className="w-[400px]">
<Textarea.Root className="w-[400px]">
<div className="text-lg font-bold leading-[22px]">Error</div>
<Textarea.Input value={value4} onValueChange={setValue4} />
<Textarea.Input value={value4} onValueChange={setValue4} hasError />
<div className="text-sm flex items-center gap-1.5 leading-4 text-destructive">
<svg
xmlns="http://www.w3.org/2000/svg"
@@ -253,7 +253,7 @@ export const RealWorldExamples: Story = {
{/* Tweet Composer */}
<div>
<h3 className="mb-3 text-sm font-semibold">Tweet Composer</h3>
<Textarea.Root error={tweetError} className="w-[500px]">
<Textarea.Root className="w-[500px]">
<div className="text-lg font-bold leading-[22px]">What's happening?</div>
<div className="relative">
<Textarea.Input
@@ -261,6 +261,7 @@ export const RealWorldExamples: Story = {
onValueChange={setTweet}
maxLength={280}
placeholder="Share your thoughts..."
hasError={!!tweetError}
/>
<Textarea.CharCount value={tweet} maxLength={280} />
</div>
@@ -309,11 +310,11 @@ export const RealWorldExamples: Story = {
{/* Contact Form */}
<div>
<h3 className="mb-3 text-sm font-semibold">Contact Us</h3>
<Textarea.Root error={messageError} className="w-[500px]">
<Textarea.Root className="w-[500px]">
<div className="text-lg font-bold leading-[22px]">
<span className="text-destructive mr-1">*</span>Message
</div>
<Textarea.Input value={message} onValueChange={setMessage} placeholder="How can we help you?" rows={6} />
<Textarea.Input value={message} onValueChange={setMessage} placeholder="How can we help you?" rows={6} hasError={!!messageError} />
{messageError ? (
<div className="text-sm flex items-center gap-1.5 leading-4 text-destructive">
<svg
@@ -360,9 +361,9 @@ export const DarkMode: Story = {
<Textarea.Input defaultValue="This is some content in dark mode" />
</Textarea.Root>
<Textarea.Root error="Error in dark mode" className="w-[400px]">
<Textarea.Root className="w-[400px]">
<div className="text-lg font-bold leading-[22px]">Error (Dark)</div>
<Textarea.Input />
<Textarea.Input hasError />
<div className="text-sm flex items-center gap-1.5 leading-4 text-destructive">
<svg
xmlns="http://www.w3.org/2000/svg"
@@ -383,9 +384,9 @@ export const DarkMode: Story = {
</div>
</Textarea.Root>
<Textarea.Root disabled className="w-[400px]">
<Textarea.Root className="w-[400px]">
<div className="text-lg font-bold leading-[22px] cursor-not-allowed opacity-70">Disabled (Dark)</div>
<Textarea.Input defaultValue="Disabled in dark mode" />
<Textarea.Input defaultValue="Disabled in dark mode" disabled />
</Textarea.Root>
</div>
</div>