From d5d0d2d96fad5168178d9c36f92c8a2b0d4cc910 Mon Sep 17 00:00:00 2001 From: yyh Date: Sat, 23 May 2026 17:30:07 +0800 Subject: [PATCH] feat(dify-ui): add textarea primitive --- packages/dify-ui/README.md | 23 +- packages/dify-ui/package.json | 4 + packages/dify-ui/src/switch/index.stories.tsx | 2 +- .../src/textarea/__tests__/index.spec.tsx | 129 ++++ .../dify-ui/src/textarea/index.stories.tsx | 141 +++++ packages/dify-ui/src/textarea/index.tsx | 119 ++++ .../delete-account/components/feed-back.tsx | 6 +- .../add-annotation-modal/edit-item/index.tsx | 4 +- .../edit-annotation-modal/edit-item/index.tsx | 4 +- .../app/app-publisher/version-info-modal.tsx | 17 +- .../config-var/config-modal/form-fields.tsx | 4 +- .../config/automatic/idea-output.tsx | 4 +- .../dataset-config/settings-modal/index.tsx | 4 +- .../debug/__tests__/chat-user-input.spec.tsx | 29 +- .../configuration/debug/chat-user-input.tsx | 5 +- .../prompt-value-panel/index.tsx | 4 +- .../components/app/create-app-modal/index.tsx | 4 +- .../app/overview/settings/index.tsx | 6 +- .../overview/workflow-hidden-input-fields.tsx | 4 +- .../chat-with-history/inputs-form/content.tsx | 4 +- .../human-input-content/content-item.tsx | 4 +- .../base/chat/chat/answer/operation.tsx | 4 +- .../embedded-chatbot/inputs-form/content.tsx | 4 +- .../follow-up-setting-modal.tsx | 4 +- .../moderation/form-generation.tsx | 4 +- .../base/form/components/field/text-area.tsx | 8 +- .../components/base/markdown-blocks/form.tsx | 4 +- .../plugins/hitl-input-block/pre-populate.tsx | 4 +- .../base/textarea/__tests__/index.spec.tsx | 77 --- .../base/textarea/index.stories.tsx | 562 ------------------ web/app/components/base/textarea/index.tsx | 60 -- .../list/template-card/edit-pipeline-info.tsx | 7 +- .../datasets/rename-modal/index.tsx | 4 +- .../form/components/basic-info-section.tsx | 4 +- .../settings/summary-index-setting.tsx | 13 +- .../explore/create-app-modal/index.tsx | 4 +- .../app-selector/app-inputs-form.tsx | 4 +- .../tool-selector/__tests__/index.spec.tsx | 8 +- .../__tests__/tool-base-form.spec.tsx | 28 +- .../components/tool-base-form.tsx | 7 +- .../__tests__/use-tool-selector-state.spec.ts | 4 +- .../hooks/use-tool-selector-state.ts | 4 +- ...blish-as-knowledge-pipeline-modal.spec.tsx | 17 +- .../publish-as-knowledge-pipeline-modal.tsx | 5 +- .../share/text-generation/run-once/index.tsx | 4 +- .../edit-custom-collection-modal/index.tsx | 4 +- .../components/tools/mcp/mcp-server-modal.tsx | 7 +- .../tools/mcp/mcp-server-param-item.tsx | 7 +- .../components/tools/workflow-tool/index.tsx | 4 +- .../components/before-run-form/form-item.tsx | 4 +- .../assigner/components/var-list/index.tsx | 4 +- .../nodes/http/components/curl-panel.tsx | 4 +- .../json-schema-generator/prompt-editor.tsx | 8 +- .../edit-card/advanced-options.tsx | 8 +- .../components/loop-variables/form-item.tsx | 8 +- .../components/extract-parameter/update.tsx | 4 +- .../variable-inspect/display-content.tsx | 4 +- .../value-content-sections.tsx | 4 +- 58 files changed, 539 insertions(+), 896 deletions(-) create mode 100644 packages/dify-ui/src/textarea/__tests__/index.spec.tsx create mode 100644 packages/dify-ui/src/textarea/index.stories.tsx create mode 100644 packages/dify-ui/src/textarea/index.tsx delete mode 100644 web/app/components/base/textarea/__tests__/index.spec.tsx delete mode 100644 web/app/components/base/textarea/index.stories.tsx delete mode 100644 web/app/components/base/textarea/index.tsx diff --git a/packages/dify-ui/README.md b/packages/dify-ui/README.md index 325454d466..fe742de068 100644 --- a/packages/dify-ui/README.md +++ b/packages/dify-ui/README.md @@ -33,6 +33,7 @@ import { Drawer, DrawerPopup, DrawerTrigger } from '@langgenius/dify-ui/drawer' import { FieldControl, FieldLabel, FieldRoot } from '@langgenius/dify-ui/field' import { Form } from '@langgenius/dify-ui/form' import { Popover, PopoverContent, PopoverTrigger } from '@langgenius/dify-ui/popover' +import { Textarea } from '@langgenius/dify-ui/textarea' import '@langgenius/dify-ui/styles.css' // once, in the app root ``` @@ -40,16 +41,16 @@ Importing from `@langgenius/dify-ui` (no subpath) is intentionally not supported ## Primitives -| Category | Subpath | Notes | -| ---------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------- | -| Actions | `./button` | Design-system CTA primitive with `cva` variants. | -| Feedback | `./meter`, `./toast` | Meter is inline status; Toast owns the `z-60` layer. | -| Form | `./form`, `./field`, `./fieldset`, `./input`, `./checkbox`, `./checkbox-group`, `./radio`, `./radio-group`, `./number-field`, `./select`, `./slider`, `./switch` | Native form boundary, field semantics, and controls. | -| Layout | `./scroll-area` | Custom-styled scrollbar over the host viewport. | -| Media | `./avatar` | Avatar root, image, and fallback primitives. | -| Navigation | `./tabs`, `./toggle-group` | Tabs for panels; ToggleGroup for segmented modes. | -| Overlay / menu | `./alert-dialog`, `./context-menu`, `./dialog`, `./drawer`, `./dropdown-menu`, `./popover`, `./preview-card`, `./tooltip` | Portalled. See [Overlay & portal contract] below. | -| Search / pickers | `./autocomplete`, `./combobox`, `./select` | Search input, searchable picker, and closed picker. | +| Category | Subpath | Notes | +| ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ---------------------------------------------------- | +| Actions | `./button` | Design-system CTA primitive with `cva` variants. | +| Feedback | `./meter`, `./toast` | Meter is inline status; Toast owns the `z-60` layer. | +| Form | `./form`, `./field`, `./fieldset`, `./input`, `./textarea`, `./checkbox`, `./checkbox-group`, `./radio`, `./radio-group`, `./number-field`, `./select`, `./slider`, `./switch` | Native form boundary, field semantics, and controls. | +| Layout | `./scroll-area` | Custom-styled scrollbar over the host viewport. | +| Media | `./avatar` | Avatar root, image, and fallback primitives. | +| Navigation | `./tabs`, `./toggle-group` | Tabs for panels; ToggleGroup for segmented modes. | +| Overlay / menu | `./alert-dialog`, `./context-menu`, `./dialog`, `./drawer`, `./dropdown-menu`, `./popover`, `./preview-card`, `./tooltip` | Portalled. See [Overlay & portal contract] below. | +| Search / pickers | `./autocomplete`, `./combobox`, `./select` | Search input, searchable picker, and closed picker. | Utilities: @@ -64,7 +65,7 @@ Use `Form` for the submit boundary. It renders a native `
`, preserves Ente Use `FieldRoot` for each standalone named field. A field must have a stable `name`, a label relationship, and either a `FieldControl` or another control that participates in the same Base UI field context. Prefer a visible label for normal form rows; when the surrounding UI already supplies the visible text, use the matching label primitive visually hidden or put `aria-label` on the actual interactive control. `FieldDescription` and `FieldError` provide the message relationships that screen readers need, while the Dify wrapper adds the default Form Input Set styling from the design system. -Choose the label primitive by the control semantics. Text-like inputs, input-based `Combobox` / `Autocomplete`, single `Checkbox` / `Radio`, `Switch`, and `NumberField` use `FieldLabel`. Trigger-based `Select` fields use `SelectLabel`; `Slider` fields use `SliderLabel`, with per-thumb `aria-label` only when the thumbs need distinct names. `SelectGroupLabel` and `AutocompleteGroupLabel` only label grouped options inside their popup content; they are not field labels. +Choose the label primitive by the control semantics. Text-like inputs, `Textarea`, input-based `Combobox` / `Autocomplete`, single `Checkbox` / `Radio`, `Switch`, and `NumberField` use `FieldLabel`. Trigger-based `Select` fields use `SelectLabel`; `Slider` fields use `SliderLabel`, with per-thumb `aria-label` only when the thumbs need distinct names. `SelectGroupLabel` and `AutocompleteGroupLabel` only label grouped options inside their popup content; they are not field labels. Use `FieldsetRoot` and `FieldsetLegend` when one field is represented by a group of related controls, such as checkbox groups, radio groups, multi-thumb sliders, or a section that combines several inputs. For checkbox and radio groups, wrap each option with `FieldItem` and give each option its own label: diff --git a/packages/dify-ui/package.json b/packages/dify-ui/package.json index b18b8f3462..24ba9d23fd 100644 --- a/packages/dify-ui/package.json +++ b/packages/dify-ui/package.json @@ -113,6 +113,10 @@ "types": "./src/tabs/index.tsx", "import": "./src/tabs/index.tsx" }, + "./textarea": { + "types": "./src/textarea/index.tsx", + "import": "./src/textarea/index.tsx" + }, "./toggle-group": { "types": "./src/toggle-group/index.tsx", "import": "./src/toggle-group/index.tsx" diff --git a/packages/dify-ui/src/switch/index.stories.tsx b/packages/dify-ui/src/switch/index.stories.tsx index 4d47ef688e..43e74f1e98 100644 --- a/packages/dify-ui/src/switch/index.stories.tsx +++ b/packages/dify-ui/src/switch/index.stories.tsx @@ -148,7 +148,7 @@ export const AllStates: Story = { parameters: { docs: { description: { - story: 'Complete variant matrix: all sizes × all states, matching Figma design spec (node 2144:1210).', + story: 'Complete variant matrix: all sizes and states.', }, }, }, diff --git a/packages/dify-ui/src/textarea/__tests__/index.spec.tsx b/packages/dify-ui/src/textarea/__tests__/index.spec.tsx new file mode 100644 index 0000000000..6dd3ddba77 --- /dev/null +++ b/packages/dify-ui/src/textarea/__tests__/index.spec.tsx @@ -0,0 +1,129 @@ +import { render } from 'vitest-browser-react' +import { + FieldDescription, + FieldError, + FieldLabel, + FieldRoot, +} from '../../field' +import { Form } from '../../form' +import { Textarea } from '../index' + +const asHTMLElement = (element: HTMLElement | SVGElement) => element as HTMLElement +const setTextareaValue = (element: HTMLElement | SVGElement, value: string) => { + const textarea = asHTMLElement(element) as HTMLTextAreaElement + const valueSetter = Object.getOwnPropertyDescriptor(window.HTMLTextAreaElement.prototype, 'value')?.set + valueSetter?.call(textarea, value) + textarea.dispatchEvent(new Event('input', { bubbles: true })) +} + +describe('Textarea', () => { + it('should render a labelled textarea through Base UI Field.Control', async () => { + const screen = await render( + + Description + - ) - }, -) -Textarea.displayName = 'Textarea' - -export default Textarea diff --git a/web/app/components/datasets/create-from-pipeline/list/template-card/edit-pipeline-info.tsx b/web/app/components/datasets/create-from-pipeline/list/template-card/edit-pipeline-info.tsx index 2cf2782185..b49699a28d 100644 --- a/web/app/components/datasets/create-from-pipeline/list/template-card/edit-pipeline-info.tsx +++ b/web/app/components/datasets/create-from-pipeline/list/template-card/edit-pipeline-info.tsx @@ -1,6 +1,7 @@ import type { AppIconSelection } from '@/app/components/base/app-icon-picker' import type { PipelineTemplate } from '@/models/pipeline' import { Button } from '@langgenius/dify-ui/button' +import { Textarea } from '@langgenius/dify-ui/textarea' import { toast } from '@langgenius/dify-ui/toast' import { RiCloseLine } from '@remixicon/react' import * as React from 'react' @@ -9,7 +10,6 @@ import { useTranslation } from 'react-i18next' import AppIcon from '@/app/components/base/app-icon' import AppIconPicker from '@/app/components/base/app-icon-picker' import Input from '@/app/components/base/input' -import Textarea from '@/app/components/base/textarea' import { useInvalidCustomizedTemplateList, useUpdateTemplateInfo } from '@/service/use-pipeline' type EditPipelineInfoProps = { @@ -57,8 +57,7 @@ const EditPipelineInfo = ({ setShowAppIconPicker(false) }, []) - const handleDescriptionChange = useCallback((event: React.ChangeEvent) => { - const value = event.target.value + const handleDescriptionChange = useCallback((value: string) => { setDescription(value) }, []) @@ -133,7 +132,7 @@ const EditPipelineInfo = ({ {t('knowledgeDescription', { ns: 'datasetPipeline' })} + onValueChange={value => setDescription(value)} + /> {latestParams.length > 0 && ( diff --git a/web/app/components/tools/mcp/mcp-server-param-item.tsx b/web/app/components/tools/mcp/mcp-server-param-item.tsx index 316bbca556..185c3413ca 100644 --- a/web/app/components/tools/mcp/mcp-server-param-item.tsx +++ b/web/app/components/tools/mcp/mcp-server-param-item.tsx @@ -1,7 +1,7 @@ 'use client' +import { Textarea } from '@langgenius/dify-ui/textarea' import * as React from 'react' import { useTranslation } from 'react-i18next' -import Textarea from '@/app/components/base/textarea' type Props = { data?: any @@ -28,9 +28,8 @@ const MCPServerParamItem = ({ className="h-8 resize-none" value={value} placeholder={t('mcp.server.modal.parametersPlaceholder', { ns: 'tools' })} - onChange={e => onChange(e.target.value)} - > - + onValueChange={value => onChange(value)} + /> ) } diff --git a/web/app/components/tools/workflow-tool/index.tsx b/web/app/components/tools/workflow-tool/index.tsx index 9da6793092..46a5cdf58a 100644 --- a/web/app/components/tools/workflow-tool/index.tsx +++ b/web/app/components/tools/workflow-tool/index.tsx @@ -14,6 +14,7 @@ import { DrawerTitle, DrawerViewport, } from '@langgenius/dify-ui/drawer' +import { Textarea } from '@langgenius/dify-ui/textarea' import { toast } from '@langgenius/dify-ui/toast' import { Tooltip, TooltipContent, TooltipTrigger } from '@langgenius/dify-ui/tooltip' import { produce } from 'immer' @@ -25,7 +26,6 @@ import Divider from '@/app/components/base/divider' import EmojiPickerInner from '@/app/components/base/emoji-picker/Inner' import { Infotip } from '@/app/components/base/infotip' import Input from '@/app/components/base/input' -import Textarea from '@/app/components/base/textarea' import LabelSelector from '@/app/components/tools/labels/selector' import ConfirmModal from '@/app/components/tools/workflow-tool/confirm-modal' import MethodSelector from '@/app/components/tools/workflow-tool/method-selector' @@ -305,7 +305,7 @@ export function WorkflowToolDrawer({