diff --git a/eslint-suppressions.json b/eslint-suppressions.json index 2624746723..c4f01d8b68 100644 --- a/eslint-suppressions.json +++ b/eslint-suppressions.json @@ -1757,14 +1757,6 @@ "count": 1 } }, - "web/app/components/base/textarea/index.stories.tsx": { - "no-console": { - "count": 1 - }, - "ts/no-explicit-any": { - "count": 1 - } - }, "web/app/components/base/voice-input/__tests__/index.spec.tsx": { "ts/no-explicit-any": { "count": 3 diff --git a/packages/dify-ui/README.md b/packages/dify-ui/README.md index dbf6dbac17..12e5915397 100644 --- a/packages/dify-ui/README.md +++ b/packages/dify-ui/README.md @@ -34,6 +34,7 @@ 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 { SegmentedControl, SegmentedControlItem } from '@langgenius/dify-ui/segmented-control' +import { Textarea } from '@langgenius/dify-ui/textarea' import '@langgenius/dify-ui/styles.css' // once, in the app root ``` @@ -41,17 +42,17 @@ Importing from `@langgenius/dify-ui` (no subpath) is intentionally not supported ## Primitives -| Category | Subpath | Notes | -| ---------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------ | -| Actions | `./button` | Design-system CTA primitive with `cva` variants. | -| Controls | `./segmented-control` | SegmentedControl for mode, filter, and view selection. | -| 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 | `./pagination`, `./tabs` | Pagination for page navigation; Tabs for panels. | -| 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. | +| Controls | `./segmented-control` | SegmentedControl for mode, filter, and view selection. | +| 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 | `./pagination`, `./tabs` | Pagination for page navigation; Tabs for panels. | +| 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: @@ -72,7 +73,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 f85210e8f6..279c45c3c6 100644 --- a/packages/dify-ui/package.json +++ b/packages/dify-ui/package.json @@ -129,6 +129,10 @@ "types": "./src/tabs/index.tsx", "import": "./src/tabs/index.tsx" }, + "./textarea": { + "types": "./src/textarea/index.tsx", + "import": "./src/textarea/index.tsx" + }, "./toast": { "types": "./src/toast/index.tsx", "import": "./src/toast/index.tsx" 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..f8a540a540 --- /dev/null +++ b/packages/dify-ui/src/textarea/__tests__/index.spec.tsx @@ -0,0 +1,187 @@ +import type { FocusEvent } from 'react' +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/billing/apps-full-in-dialog/index.tsx b/web/app/components/billing/apps-full-in-dialog/index.tsx index 08036d055a..486b203631 100644 --- a/web/app/components/billing/apps-full-in-dialog/index.tsx +++ b/web/app/components/billing/apps-full-in-dialog/index.tsx @@ -25,6 +25,7 @@ const AppsFull: FC<{ loc: string, className?: string }> = ({ const total = plan.total.buildApps const percent = total > 0 ? (usage / total) * 100 : 0 const tone: MeterTone = percent >= 80 ? 'error' : percent >= 50 ? 'warning' : 'neutral' + const buildAppsLabel = t('usagePage.buildApps', { ns: 'billing' }) return (
= ({
-
{t('usagePage.buildApps', { ns: 'billing' })}
+
{buildAppsLabel}
{usage} / {total}
- + diff --git a/web/app/components/billing/usage-info/__tests__/index.spec.tsx b/web/app/components/billing/usage-info/__tests__/index.spec.tsx index 3322d35809..b44db314fa 100644 --- a/web/app/components/billing/usage-info/__tests__/index.spec.tsx +++ b/web/app/components/billing/usage-info/__tests__/index.spec.tsx @@ -229,7 +229,7 @@ describe('UsageInfo', () => { />, ) - expect(screen.getByRole('meter')).toBeInTheDocument() + expect(screen.getByRole('meter', { name: 'Storage' })).toBeInTheDocument() expect(container.querySelector('[aria-hidden="true"]')).toBeNull() }) @@ -270,7 +270,7 @@ describe('UsageInfo', () => { />, ) - expect(screen.getByRole('meter')).toBeInTheDocument() + expect(screen.getByRole('meter', { name: 'Storage' })).toBeInTheDocument() expect(container.querySelector('[aria-hidden="true"]')).toBeNull() }) diff --git a/web/app/components/billing/usage-info/index.tsx b/web/app/components/billing/usage-info/index.tsx index 6129c92a4e..56166ce631 100644 --- a/web/app/components/billing/usage-info/index.tsx +++ b/web/app/components/billing/usage-info/index.tsx @@ -144,13 +144,13 @@ const UsageInfo: FC = ({
) : ( - + @@ -162,7 +162,7 @@ const UsageInfo: FC = ({ return ( {children}
} /> - + {storageTooltip} 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 e5622806b8..da328d0d7b 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 = { @@ -45,8 +45,7 @@ const EditPipelineInfo = ({ setAppIcon(icon) }, []) - const handleDescriptionChange = useCallback((event: React.ChangeEvent) => { - const value = event.target.value + const handleDescriptionChange = useCallback((value: string) => { setDescription(value) }, []) @@ -121,7 +120,8 @@ 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..aed4378ebf 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 @@ -25,12 +25,12 @@ const MCPServerParamItem = ({
{data.type}
+ 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 8924ed7a29..29d9a7f72e 100644 --- a/web/app/components/tools/workflow-tool/index.tsx +++ b/web/app/components/tools/workflow-tool/index.tsx @@ -13,6 +13,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' @@ -23,7 +24,6 @@ import AppIcon from '@/app/components/base/app-icon' import AppIconPicker from '@/app/components/base/app-icon-picker' 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' @@ -256,9 +256,10 @@ export function WorkflowToolDrawer({
{t('createTool.description', { ns: 'tools' })}