Skip to content

Commit d8b8682

Browse files
Textarea design migration (harness#1589)
* add textarea component design migration * add form textarea * fixes * fixes * fix counter
1 parent 51b8aa5 commit d8b8682

File tree

63 files changed

+779
-524
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

63 files changed

+779
-524
lines changed

apps/portal/src/components/docs-page/example.tsx

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import {
66
useState,
77
} from "react";
88
import { LiveEditor, LivePreview, LiveProvider } from "react-live";
9-
import { Icon } from "@harnessio/ui/components";
9+
import { Icon, Tooltip } from "@harnessio/ui/components";
1010
import { RouterContextProvider } from "@harnessio/ui/context";
1111
import ExampleLayout from "./example-layout";
1212
import { themes } from "prism-react-renderer";
@@ -61,7 +61,9 @@ const Example: FC<ExampleProps> = ({ code, scope }) => {
6161
path: "*",
6262
element: (
6363
<RouterContextProvider Link={Link} NavLink={NavLink} Outlet={Outlet}>
64-
<LivePreview />
64+
<Tooltip.Provider>
65+
<LivePreview />
66+
</Tooltip.Provider>
6567
</RouterContextProvider>
6668
),
6769
},

apps/portal/src/content/docs/components/label.mdx

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,14 @@ import { DocsPage } from "../../../components/docs-page";
3333
<input id="password" type="password" />
3434
</div>
3535
36+
<hr />
37+
38+
<div className="flex flex-col gap-4">
39+
<h5>With informer</h5>
40+
<Label htmlFor="2" informerContent="Content here">Description</Label>
41+
<input id="2" type="text" />
42+
</div>
43+
3644
</div>`}
3745
/>
3846

apps/portal/src/content/docs/components/textarea.mdx

Lines changed: 46 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -6,17 +6,55 @@ beta: true
66

77
The `Textarea` component is used to create a multi-line text input field.
88

9-
import { DocsPage } from "../../../components/docs-page";
9+
import { DocsPage } from "@/components/docs-page";
1010

1111
<DocsPage.ComponentExample
1212
client:only
13-
code={`<Textarea
14-
name="about-you"
15-
label="About you"
16-
placeholder="Tell me about yourself..."
17-
caption="What do you like to do in your free time?"
18-
optional
19-
/>`}
13+
code={`<div className="grid gap-4 max-w-96 w-full">
14+
<Textarea
15+
id="1"
16+
name="default"
17+
label="Default"
18+
placeholder="Placeholder"
19+
caption="Caption text"
20+
rows={4}
21+
/>
22+
<Textarea
23+
id="2"
24+
name="resizable"
25+
label="Resizable"
26+
placeholder="Placeholder"
27+
caption="Caption text"
28+
defaultValue="Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum."
29+
optional
30+
resizable
31+
rows={4}
32+
/>
33+
<Textarea
34+
id="3"
35+
name="error"
36+
label="This component in error state and it has a very long text"
37+
placeholder="Placeholder"
38+
maxCharacters={100}
39+
theme="danger"
40+
defaultValue="Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum."
41+
error="Error message"
42+
optional
43+
rows={4}
44+
/>
45+
<Textarea
46+
id="4"
47+
name="disabled"
48+
label="Disabled"
49+
placeholder="Placeholder"
50+
maxCharacters={100}
51+
defaultValue="Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum."
52+
caption="Caption text"
53+
optional
54+
rows={4}
55+
disabled
56+
/>
57+
</div>`}
2058
/>
2159

2260
## Usage
@@ -44,21 +82,6 @@ the value changes. When uncontrolled, the `Textarea` will use the `defaultValue`
4482
its initial value and will call `onChange` when its value changes. The `Textarea` also supports
4583
all attributes of the `textarea` HTML element.
4684

47-
```typescript jsx
48-
<Textarea
49-
name="textarea-name" // [OPTIONAL] name of the textarea
50-
id="textarea-id" // [OPTIONAL] id of the textarea
51-
value="Textarea value" // [OPTIONAL] value of the textarea
52-
onChange={handleChange} // [OPTIONAL] callback to call when the value changes
53-
label="Textarea label" // [OPTIONAL] label for the textarea
54-
caption="Textarea caption" // [OPTIONAL] caption to describe the textarea
55-
error="Error message" // [OPTIONAL] error message to display below
56-
optional // [OPTIONAL] indicate if the textarea is optional
57-
resizable // [OPTIONAL] indicate if the textarea should be resizable
58-
disabled // [OPTIONAL] indicate if the textarea is disabled
59-
/>
60-
```
61-
6285
<DocsPage.PropsTable
6386
props={[
6487
{

packages/ui/src/components/commit-suggestions-dialog/commit-suggestions-dialog.tsx

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { FC } from 'react'
22
import { useForm } from 'react-hook-form'
33

4-
import { Button, ButtonGroup, ControlGroup, Dialog, FormInput, FormWrapper, Textarea } from '@/components'
4+
import { Button, ButtonGroup, ControlGroup, Dialog, FormInput, FormWrapper } from '@/components'
55
import { UsererrorError } from '@/types'
66
import { zodResolver } from '@hookform/resolvers/zod'
77
import { z } from 'zod'
@@ -44,11 +44,7 @@ export const CommitSuggestionsDialog: FC<CommitSuggestionsDialogProps> = ({
4444
}
4545
})
4646

47-
const {
48-
register,
49-
handleSubmit,
50-
formState: { errors }
51-
} = formMethods
47+
const { register, handleSubmit } = formMethods
5248

5349
return (
5450
<Dialog.Root open={isOpen} onOpenChange={onClose}>
@@ -65,12 +61,11 @@ export const CommitSuggestionsDialog: FC<CommitSuggestionsDialogProps> = ({
6561
{...register('title')}
6662
placeholder={commitTitlePlaceHolder ?? 'Add a commit message'}
6763
/>
68-
<Textarea
64+
<FormInput.Textarea
6965
id="message"
7066
{...register('message')}
7167
placeholder="Add an optional extended description"
7268
label="Extended description"
73-
error={errors.message?.message?.toString()}
7469
/>
7570
</ControlGroup>
7671

packages/ui/src/components/form-input/components/form-text-input.tsx

Lines changed: 4 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { forwardRef, useRef } from 'react'
1+
import { forwardRef } from 'react'
22
import { Controller, useFormContext } from 'react-hook-form'
33

44
import { TextInput, type TextInputProps } from '@components/inputs'
@@ -10,20 +10,6 @@ interface FormTextInputPropsType extends TextInputProps {
1010
const FormTextInput = forwardRef<HTMLInputElement, FormTextInputPropsType>((props, ref) => {
1111
const formContext = useFormContext()
1212

13-
const inputRef = useRef<HTMLInputElement | null>(null)
14-
15-
const setRefs = (element: HTMLInputElement | null) => {
16-
// Save to local ref
17-
inputRef.current = element
18-
19-
// Forward to external ref
20-
if (typeof ref === 'function') {
21-
ref(element)
22-
} else if (ref) {
23-
ref.current = element
24-
}
25-
}
26-
2713
// Only access the component if it is inside FormProvider component tree
2814
if (!formContext) {
2915
throw new Error(
@@ -35,15 +21,9 @@ const FormTextInput = forwardRef<HTMLInputElement, FormTextInputPropsType>((prop
3521
<Controller
3622
name={props.name}
3723
control={formContext.control}
38-
render={({ field, fieldState }) => {
39-
// Use proper React Hook Form reference handling
40-
field.ref = setRefs
41-
42-
// form error takes precedence over props.error
43-
const errorMessage = fieldState.error?.message || props.error
44-
45-
return <TextInput {...props} {...field} error={errorMessage} />
46-
}}
24+
render={({ field, fieldState }) => (
25+
<TextInput {...props} {...field} error={fieldState.error?.message || props.error} ref={ref} />
26+
)}
4727
/>
4828
)
4929
})
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import { forwardRef } from 'react'
2+
import { Controller, useFormContext } from 'react-hook-form'
3+
4+
import { Textarea, TextareaProps } from '@components/form-primitives'
5+
6+
interface FormTextareaPropsType extends TextareaProps {
7+
name: string
8+
}
9+
10+
const FormTextarea = forwardRef<HTMLTextAreaElement, FormTextareaPropsType>((props, ref) => {
11+
const formContext = useFormContext()
12+
13+
if (!formContext) {
14+
throw new Error(
15+
'FormTextarea must be used within a FormProvider context through FormWrapper. Use the standalone Textarea component if form integration is not required.'
16+
)
17+
}
18+
19+
return (
20+
<Controller
21+
name={props.name}
22+
control={formContext.control}
23+
render={({ field, fieldState }) => (
24+
<Textarea {...props} {...field} error={fieldState.error?.message || props.error} ref={ref} />
25+
)}
26+
/>
27+
)
28+
})
29+
30+
FormTextarea.displayName = 'FormInput.Textarea'
31+
32+
export { FormTextarea, type FormTextareaPropsType }
Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
import { FormMultiSelect, type FormMultiSelectPropsType } from './components/form-multi-select-v2'
22
import { FormTextInput, type FormTextInputPropsType } from './components/form-text-input'
3+
import { FormTextarea } from './components/form-textarea'
34

45
const FormInput = {
56
Text: FormTextInput,
6-
MultiSelect: FormMultiSelect
7+
MultiSelect: FormMultiSelect,
8+
Textarea: FormTextarea
79
}
810

911
export { FormInput, type FormTextInputPropsType, type FormMultiSelectPropsType }

packages/ui/src/components/form-primitives/form-caption.tsx

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,18 @@ const formCaptionVariants = cva('cn-caption', {
2121
type FormCaptionProps = {
2222
theme?: VariantProps<typeof formCaptionVariants>['theme']
2323
className?: string
24+
disabled?: boolean
2425
}
2526

26-
export const FormCaption = ({ theme = 'default', className, children }: PropsWithChildren<FormCaptionProps>) => {
27+
export const FormCaption = ({
28+
theme = 'default',
29+
className,
30+
disabled,
31+
children
32+
}: PropsWithChildren<FormCaptionProps>) => {
33+
/**
34+
* Return null if no message, errorMessage, or warningMessage is provided
35+
*/
2736
if (!children) {
2837
return null
2938
}
@@ -37,7 +46,7 @@ export const FormCaption = ({ theme = 'default', className, children }: PropsWit
3746
const effectiveIconName = theme === 'danger' ? 'cross-circle' : 'warning-triangle-outline'
3847

3948
return (
40-
<p className={cn(formCaptionVariants({ theme }), className)}>
49+
<p className={cn(formCaptionVariants({ theme }), { 'cn-caption-disabled': disabled }, className)}>
4150
{canShowIcon && <Icon name={effectiveIconName} size={14} />}
4251
<span>{children}</span>
4352
</p>

packages/ui/src/components/form-primitives/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,5 +6,6 @@ export * from './legend'
66
export * from './message'
77
export * from './separator'
88
export * from './form-wrapper'
9+
export * from './textarea'
910
export * from './form-primitives.types'
1011
export * from './form-caption'

packages/ui/src/components/form-primitives/label.tsx

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,11 @@ import * as LabelPrimitive from '@radix-ui/react-label'
66
import { cn } from '@utils/cn'
77
import { cva, type VariantProps } from 'class-variance-authority'
88

9-
const labelVariants = cva('label', {
9+
const labelVariants = cva('cn-label', {
1010
variants: {
1111
variant: {
12-
default: 'label-default',
13-
primary: 'label-primary'
12+
default: 'cn-label-default',
13+
primary: 'cn-label-primary'
1414
}
1515
},
1616
defaultVariants: {
@@ -27,19 +27,24 @@ export type LabelProps = Omit<ComponentPropsWithoutRef<typeof LabelPrimitive.Roo
2727
}
2828

2929
const Label = forwardRef<ElementRef<typeof LabelPrimitive.Root>, LabelProps>(
30-
({ className, children, variant = 'default', optional, disabled, informerContent, ...props }, ref) => {
30+
({ className, children, variant = 'default', optional, disabled, informerContent, informerProps, ...props }, ref) => {
3131
const LabelComponent = ({ className }: { className?: string }) => (
32-
<LabelPrimitive.Root ref={ref} className={cn(labelVariants({ variant }), className)} {...props}>
33-
{children} {optional && <span className="label-optional">(optional)</span>}
32+
<LabelPrimitive.Root
33+
ref={ref}
34+
className={cn(labelVariants({ variant }), { 'cn-label-disabled': disabled }, className)}
35+
{...props}
36+
>
37+
<span className="cn-label-text">{children}</span>
38+
{optional && <span className="cn-label-optional">(optional)</span>}
3439
</LabelPrimitive.Root>
3540
)
3641

3742
if (informerContent) {
3843
return (
39-
<span className={cn('flex items-center gap-1', className)}>
44+
<span className={cn('cn-label-container', className)}>
4045
<LabelComponent />
4146

42-
<Informer className="label-informer" disabled={disabled}>
47+
<Informer {...informerProps} className="cn-label-informer" disabled={disabled}>
4348
{informerContent}
4449
</Informer>
4550
</span>

0 commit comments

Comments
 (0)