feat(textarea): remove TextareaContext and update stories to reflect new error handling
This commit is contained in:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user