mirror of
https://github.com/langgenius/dify.git
synced 2026-10-16 15:35:32 +08:00
fix(web): delegate overlay initial focus to popup owners (#42121)
This commit is contained in:
@@ -3751,9 +3751,6 @@
|
||||
"jsx-a11y/click-events-have-key-events": {
|
||||
"count": 1
|
||||
},
|
||||
"jsx-a11y/no-autofocus": {
|
||||
"count": 1
|
||||
},
|
||||
"jsx-a11y/no-static-element-interactions": {
|
||||
"count": 1
|
||||
}
|
||||
@@ -5139,7 +5136,7 @@
|
||||
},
|
||||
"web/app/components/workflow/workflow-generator/index.tsx": {
|
||||
"dify/require-disable-directive-description": {
|
||||
"count": 2
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"web/app/components/workflow/workflow-preview/components/nodes/constants.ts": {
|
||||
|
||||
@@ -267,7 +267,6 @@ const ComponentPicker = ({
|
||||
onBlur={handleClose}
|
||||
showManageInputField={workflowVariableBlock.showManageInputField}
|
||||
onManageInputField={workflowVariableBlock.onManageInputField}
|
||||
autoFocus={false}
|
||||
isInCodeGeneratorInstructionEditor={
|
||||
currentBlock?.generatorType === GeneratorType.code
|
||||
}
|
||||
|
||||
+3
-1
@@ -290,11 +290,12 @@ describe('InstallFromGitHub', () => {
|
||||
// Rendering Tests
|
||||
// ================================
|
||||
describe('Rendering', () => {
|
||||
it('should render modal with correct initial state for new installation', () => {
|
||||
it('should render and focus the URL for a new installation', async () => {
|
||||
render(<InstallFromGitHub {...defaultProps} />)
|
||||
|
||||
expect(getRepoUrlInput()).toBeInTheDocument()
|
||||
expect(getRepoUrlInput()).toHaveValue('')
|
||||
await waitFor(() => expect(getRepoUrlInput()).toHaveFocus())
|
||||
})
|
||||
|
||||
it('should render modal with selectPackage step when updatePayload is provided', () => {
|
||||
@@ -559,6 +560,7 @@ describe('InstallFromGitHub', () => {
|
||||
await waitFor(() => {
|
||||
expect(getRepoUrlInput()).toBeInTheDocument()
|
||||
})
|
||||
expect(getRepoUrlInput()).not.toHaveFocus()
|
||||
})
|
||||
|
||||
it('should go back from readyToInstall to selectPackage', async () => {
|
||||
|
||||
@@ -10,7 +10,7 @@ import { IconButton } from '@langgenius/dify-ui/icon-button'
|
||||
import { Input } from '@langgenius/dify-ui/input'
|
||||
import { toast } from '@langgenius/dify-ui/toast'
|
||||
import * as React from 'react'
|
||||
import { useCallback, useState } from 'react'
|
||||
import { useCallback, useRef, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import useGetIcon from '@/app/components/plugins/install-plugin/base/use-get-icon'
|
||||
import { InstallStepFromGitHub } from '../../types'
|
||||
@@ -42,6 +42,7 @@ const InstallFromGitHub: React.FC<InstallFromGitHubProps> = ({
|
||||
onClose,
|
||||
onSuccess,
|
||||
}) => {
|
||||
const repoUrlInputRef = useRef<HTMLInputElement>(null)
|
||||
const { t } = useTranslation()
|
||||
const { getIconUrl } = useGetIcon()
|
||||
const { refreshPluginList } = useRefreshPluginList()
|
||||
@@ -178,6 +179,7 @@ const InstallFromGitHub: React.FC<InstallFromGitHubProps> = ({
|
||||
}}
|
||||
>
|
||||
<DialogContent
|
||||
initialFocus={repoUrlInputRef}
|
||||
backdropProps={{ forceRender: true }}
|
||||
className={cn(
|
||||
'w-140 overflow-hidden! text-left align-middle',
|
||||
@@ -245,8 +247,7 @@ const InstallFromGitHub: React.FC<InstallFromGitHubProps> = ({
|
||||
</span>
|
||||
</FieldLabel>
|
||||
<Input
|
||||
// oxlint-disable-next-line jsx-a11y/no-autofocus -- The dialog opens from an explicit install command, and the repository URL is its primary field.
|
||||
autoFocus
|
||||
ref={repoUrlInputRef}
|
||||
type="text"
|
||||
inputMode="url"
|
||||
autoComplete="off"
|
||||
|
||||
+1
@@ -117,6 +117,7 @@ describe('VarReferencePicker', () => {
|
||||
|
||||
fireEvent.click(screen.getByTestId('var-reference-picker-trigger'))
|
||||
|
||||
await waitFor(() => expect(screen.getByRole('searchbox')).toHaveFocus())
|
||||
fireEvent.click(await screen.findByText('answer'))
|
||||
|
||||
expect(onChange).toHaveBeenCalledWith(
|
||||
|
||||
@@ -146,6 +146,7 @@ const VarReferencePicker: FC<Props> = ({
|
||||
const isInLoop = !!node?.data.isInLoop
|
||||
const loopNode = isInLoop ? (nodes.find((n) => n.id === node?.parentId) ?? null) : null
|
||||
|
||||
const searchInputRef = useRef<HTMLInputElement>(null)
|
||||
const triggerRef = useRef<HTMLDivElement>(null)
|
||||
const [triggerWidth, setTriggerWidth] = useState(TRIGGER_DEFAULT_WIDTH)
|
||||
useEffect(() => {
|
||||
@@ -419,12 +420,14 @@ const VarReferencePicker: FC<Props> = ({
|
||||
/>
|
||||
)}
|
||||
<PopoverContent
|
||||
initialFocus={searchInputRef}
|
||||
placement={isAddBtnTrigger ? 'bottom-end' : 'bottom-start'}
|
||||
sideOffset={4}
|
||||
className="border-none bg-transparent p-0 shadow-none backdrop-blur-none"
|
||||
>
|
||||
{!isConstant && (
|
||||
<VarReferencePopup
|
||||
searchInputRef={searchInputRef}
|
||||
vars={outputVars}
|
||||
popupFor={popupFor}
|
||||
onChange={handleVarReferenceChange}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import type { FC, RefObject } from 'react'
|
||||
import type { NodeOutPutVar, ValueSelector, Var } from '@/app/components/workflow/types'
|
||||
import * as React from 'react'
|
||||
import { useMemo } from 'react'
|
||||
@@ -9,6 +9,7 @@ import { useStore } from '@/app/components/workflow/store'
|
||||
import VarReferenceVars from './var-reference-vars'
|
||||
|
||||
type Props = Readonly<{
|
||||
searchInputRef?: RefObject<HTMLInputElement | null>
|
||||
vars: NodeOutPutVar[]
|
||||
popupFor?: 'assigned' | 'toAssigned'
|
||||
onChange: (value: ValueSelector, varDetail: Var) => void
|
||||
@@ -17,6 +18,7 @@ type Props = Readonly<{
|
||||
preferSchemaType?: boolean
|
||||
}>
|
||||
const VarReferencePopup: FC<Props> = ({
|
||||
searchInputRef,
|
||||
vars,
|
||||
popupFor,
|
||||
onChange,
|
||||
@@ -59,6 +61,7 @@ const VarReferencePopup: FC<Props> = ({
|
||||
)
|
||||
) : (
|
||||
<VarReferenceVars
|
||||
searchInputRef={searchInputRef}
|
||||
searchBoxClassName="mt-1"
|
||||
vars={vars}
|
||||
onChange={onChange}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import type { FC, RefObject } from 'react'
|
||||
import type { Field, StructuredOutput } from '../../../llm/types'
|
||||
import type { NodeOutPutVar, ValueSelector, Var } from '@/app/components/workflow/types'
|
||||
import { cn } from '@langgenius/dify-ui/cn'
|
||||
@@ -334,7 +334,7 @@ type Props = Readonly<{
|
||||
isInCodeGeneratorInstructionEditor?: boolean
|
||||
showManageInputField?: boolean
|
||||
onManageInputField?: () => void
|
||||
autoFocus?: boolean
|
||||
searchInputRef?: RefObject<HTMLInputElement | null>
|
||||
preferSchemaType?: boolean
|
||||
}>
|
||||
const VarReferenceVars: FC<Props> = ({
|
||||
@@ -351,13 +351,14 @@ const VarReferenceVars: FC<Props> = ({
|
||||
isInCodeGeneratorInstructionEditor,
|
||||
showManageInputField,
|
||||
onManageInputField,
|
||||
autoFocus = true,
|
||||
searchInputRef: externalSearchInputRef,
|
||||
preferSchemaType,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const [internalSearchValue, setInternalSearchValue] = useState('')
|
||||
const listRef = useRef<HTMLDivElement>(null)
|
||||
const searchInputRef = useRef<HTMLInputElement>(null)
|
||||
const internalSearchInputRef = useRef<HTMLInputElement>(null)
|
||||
const searchInputRef = externalSearchInputRef ?? internalSearchInputRef
|
||||
const searchValue = searchText ?? internalSearchValue
|
||||
const searchLabel = t(($) => $['common.searchVar'], { ns: 'workflow' })
|
||||
const filteredVars = useMemo(() => filterReferenceVars(vars, searchValue), [vars, searchValue])
|
||||
@@ -497,7 +498,6 @@ const VarReferenceVars: FC<Props> = ({
|
||||
placeholder={searchLabel}
|
||||
onValueChange={setInternalSearchValue}
|
||||
onKeyDown={handleKeyDown}
|
||||
autoFocus={autoFocus}
|
||||
/>
|
||||
<InputGroupAddon className="ps-2 pe-0.5">
|
||||
<span
|
||||
|
||||
@@ -3,7 +3,7 @@ import type { NodeOutPutVar, ValueSelector, Var } from '@/app/components/workflo
|
||||
import { Button } from '@langgenius/dify-ui/button'
|
||||
import { Popover, PopoverContent, PopoverTrigger } from '@langgenius/dify-ui/popover'
|
||||
import { RiAddLine } from '@remixicon/react'
|
||||
import { useCallback, useState } from 'react'
|
||||
import { useCallback, useRef, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import VarReferenceVars from '@/app/components/workflow/nodes/_base/components/variable/var-reference-vars'
|
||||
|
||||
@@ -22,6 +22,7 @@ const ConditionAdd = ({
|
||||
onSelectVariable,
|
||||
disabled,
|
||||
}: ConditionAddProps) => {
|
||||
const searchInputRef = useRef<HTMLInputElement>(null)
|
||||
const { t } = useTranslation()
|
||||
const [open, setOpen] = useState(false)
|
||||
|
||||
@@ -47,12 +48,18 @@ const ConditionAdd = ({
|
||||
}}
|
||||
/>
|
||||
<PopoverContent
|
||||
initialFocus={searchInputRef}
|
||||
placement="bottom-start"
|
||||
sideOffset={4}
|
||||
className="border-none bg-transparent p-0 shadow-none backdrop-blur-none"
|
||||
>
|
||||
<div className="w-74 rounded-lg border-[0.5px] border-components-panel-border bg-components-panel-bg-blur shadow-lg">
|
||||
<VarReferenceVars vars={variables} isSupportFileVar onChange={handleSelectVariable} />
|
||||
<VarReferenceVars
|
||||
searchInputRef={searchInputRef}
|
||||
vars={variables}
|
||||
isSupportFileVar
|
||||
onChange={handleSelectVariable}
|
||||
/>
|
||||
</div>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
|
||||
+10
-1
@@ -6,6 +6,7 @@ import type {
|
||||
VarType,
|
||||
} from '@/app/components/workflow/types'
|
||||
import { Popover, PopoverContent, PopoverTrigger } from '@langgenius/dify-ui/popover'
|
||||
import { useRef } from 'react'
|
||||
import VariableTag from '@/app/components/workflow/nodes/_base/components/variable-tag'
|
||||
import VarReferenceVars from '@/app/components/workflow/nodes/_base/components/variable/var-reference-vars'
|
||||
|
||||
@@ -28,9 +29,11 @@ const ConditionVarSelector = ({
|
||||
nodesOutputVars,
|
||||
onChange,
|
||||
}: ConditionVarSelectorProps) => {
|
||||
const searchInputRef = useRef<HTMLInputElement>(null)
|
||||
return (
|
||||
<Popover open={open} onOpenChange={onOpenChange}>
|
||||
<PopoverTrigger
|
||||
// TODO: Declare non-native button semantics for this div trigger to support keyboard activation.
|
||||
render={
|
||||
<div className="w-full cursor-pointer">
|
||||
<VariableTag
|
||||
@@ -43,12 +46,18 @@ const ConditionVarSelector = ({
|
||||
}
|
||||
/>
|
||||
<PopoverContent
|
||||
initialFocus={searchInputRef}
|
||||
placement="bottom-start"
|
||||
sideOffset={4}
|
||||
className="border-none bg-transparent p-0 shadow-none backdrop-blur-none"
|
||||
>
|
||||
<div className="w-74 rounded-lg border-[0.5px] border-components-panel-border bg-components-panel-bg-blur shadow-lg">
|
||||
<VarReferenceVars vars={nodesOutputVars} isSupportFileVar onChange={onChange} />
|
||||
<VarReferenceVars
|
||||
searchInputRef={searchInputRef}
|
||||
vars={nodesOutputVars}
|
||||
isSupportFileVar
|
||||
onChange={onChange}
|
||||
/>
|
||||
</div>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
|
||||
@@ -12,7 +12,7 @@ import { Popover, PopoverContent, PopoverTrigger } from '@langgenius/dify-ui/pop
|
||||
import { RiArrowDownSLine } from '@remixicon/react'
|
||||
import { useBoolean } from 'ahooks'
|
||||
import { capitalize } from 'es-toolkit/string'
|
||||
import { memo, useCallback, useState } from 'react'
|
||||
import { memo, useCallback, useRef, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { Variable02 } from '@/app/components/base/icons/src/vender/solid/development'
|
||||
import VarReferenceVars from '@/app/components/workflow/nodes/_base/components/variable/var-reference-vars'
|
||||
@@ -41,6 +41,7 @@ const ConditionNumberInput = ({
|
||||
isShort,
|
||||
unit,
|
||||
}: ConditionNumberInputProps) => {
|
||||
const searchInputRef = useRef<HTMLInputElement>(null)
|
||||
const { t } = useTranslation()
|
||||
const [numberVarTypeVisible, setNumberVarTypeVisible] = useState(false)
|
||||
const [variableSelectorVisible, setVariableSelectorVisible] = useState(false)
|
||||
@@ -106,6 +107,7 @@ const ConditionNumberInput = ({
|
||||
)}
|
||||
</PopoverTrigger>
|
||||
<PopoverContent
|
||||
initialFocus={searchInputRef}
|
||||
placement="bottom-start"
|
||||
sideOffset={2}
|
||||
className="border-none bg-transparent shadow-none"
|
||||
@@ -116,7 +118,11 @@ const ConditionNumberInput = ({
|
||||
isShort && 'w-50',
|
||||
)}
|
||||
>
|
||||
<VarReferenceVars vars={variables} onChange={handleSelectVariable} />
|
||||
<VarReferenceVars
|
||||
searchInputRef={searchInputRef}
|
||||
vars={variables}
|
||||
onChange={handleSelectVariable}
|
||||
/>
|
||||
</div>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
|
||||
+10
-2
@@ -1,6 +1,6 @@
|
||||
import type { Node, NodeOutPutVar, ValueSelector, Var } from '@/app/components/workflow/types'
|
||||
import { Popover, PopoverContent, PopoverTrigger } from '@langgenius/dify-ui/popover'
|
||||
import { useCallback, useState } from 'react'
|
||||
import { useCallback, useRef, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { Variable02 } from '@/app/components/base/icons/src/vender/solid/development'
|
||||
import VariableTag from '@/app/components/workflow/nodes/_base/components/variable-tag'
|
||||
@@ -22,6 +22,7 @@ const ConditionVariableSelector = ({
|
||||
nodesOutputVars = [],
|
||||
onChange,
|
||||
}: ConditionVariableSelectorProps) => {
|
||||
const searchInputRef = useRef<HTMLInputElement>(null)
|
||||
const { t } = useTranslation()
|
||||
const [open, setOpen] = useState(false)
|
||||
|
||||
@@ -36,6 +37,7 @@ const ConditionVariableSelector = ({
|
||||
return (
|
||||
<Popover open={open} onOpenChange={setOpen}>
|
||||
<PopoverTrigger
|
||||
// TODO: Declare non-native button semantics for this div trigger to support keyboard activation.
|
||||
render={
|
||||
<div className="flex h-6 grow cursor-pointer items-center">
|
||||
{!!valueSelector.length && (
|
||||
@@ -63,12 +65,18 @@ const ConditionVariableSelector = ({
|
||||
}
|
||||
/>
|
||||
<PopoverContent
|
||||
initialFocus={searchInputRef}
|
||||
placement="bottom-start"
|
||||
sideOffset={4}
|
||||
className="border-none bg-transparent p-0 shadow-none backdrop-blur-none"
|
||||
>
|
||||
<div className="w-74 rounded-lg border-[0.5px] border-components-panel-border bg-components-panel-bg-blur shadow-lg">
|
||||
<VarReferenceVars vars={nodesOutputVars} isSupportFileVar onChange={handleChange} />
|
||||
<VarReferenceVars
|
||||
searchInputRef={searchInputRef}
|
||||
vars={nodesOutputVars}
|
||||
isSupportFileVar
|
||||
onChange={handleChange}
|
||||
/>
|
||||
</div>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
import type { NodeOutPutVar } from '@/app/components/workflow/types'
|
||||
import { render, screen } from '@testing-library/react'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
import { useState } from 'react'
|
||||
import { ReactFlowProvider } from 'reactflow'
|
||||
import { VarType as NumberVarType } from '@/app/components/workflow/nodes/tool/types'
|
||||
import { VarType } from '@/app/components/workflow/types'
|
||||
import ConditionAdd from '../components/condition-add'
|
||||
import ConditionVarSelector from '../components/condition-list/condition-var-selector'
|
||||
import ConditionNumberInput from '../components/condition-number-input'
|
||||
|
||||
const answer = { variable: 'answer', type: VarType.number }
|
||||
const variables: NodeOutPutVar[] = [{ nodeId: 'source', title: 'Source', vars: [answer] }]
|
||||
|
||||
it('adds the variable selected through the search and closes the add popup', async () => {
|
||||
const user = userEvent.setup()
|
||||
const onSelectVariable = vi.fn()
|
||||
render(<ConditionAdd variables={variables} onSelectVariable={onSelectVariable} />)
|
||||
await user.click(screen.getByRole('button', { name: 'workflow.nodes.ifElse.addCondition' }))
|
||||
const search = screen.getByRole('searchbox')
|
||||
await user.type(search, 'answer{Enter}')
|
||||
expect(onSelectVariable).toHaveBeenCalledWith(['source', 'answer'], answer)
|
||||
expect(screen.queryByRole('searchbox')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('writes a variable expression when selecting a numeric comparison value', async () => {
|
||||
const user = userEvent.setup()
|
||||
const onValueChange = vi.fn()
|
||||
render(
|
||||
<ConditionNumberInput
|
||||
numberVarType={NumberVarType.variable}
|
||||
value=""
|
||||
variables={variables}
|
||||
onNumberVarTypeChange={vi.fn()}
|
||||
onValueChange={onValueChange}
|
||||
/>,
|
||||
)
|
||||
await user.click(screen.getByRole('button', { name: 'workflow.nodes.ifElse.selectVariable' }))
|
||||
await user.type(screen.getByRole('searchbox'), 'answer{Enter}')
|
||||
expect(onValueChange).toHaveBeenCalledWith('{{#source.answer#}}')
|
||||
expect(screen.queryByRole('searchbox')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('updates the selected condition variable through its controlled popup', async () => {
|
||||
const user = userEvent.setup()
|
||||
const onChange = vi.fn()
|
||||
function ConditionHarness() {
|
||||
const [open, setOpen] = useState(false)
|
||||
return (
|
||||
<ConditionVarSelector
|
||||
open={open}
|
||||
onOpenChange={setOpen}
|
||||
valueSelector={['source', 'previous']}
|
||||
varType={VarType.number}
|
||||
availableNodes={[]}
|
||||
nodesOutputVars={variables}
|
||||
onChange={onChange}
|
||||
/>
|
||||
)
|
||||
}
|
||||
render(
|
||||
<ReactFlowProvider>
|
||||
<ConditionHarness />
|
||||
</ReactFlowProvider>,
|
||||
)
|
||||
await user.click(screen.getByText('previous'))
|
||||
await user.type(screen.getByRole('searchbox'), 'answer{Enter}')
|
||||
expect(onChange).toHaveBeenCalledWith(['source', 'answer'], answer)
|
||||
})
|
||||
@@ -3,7 +3,7 @@ import type { NodeOutPutVar, ValueSelector, Var } from '@/app/components/workflo
|
||||
import { Button } from '@langgenius/dify-ui/button'
|
||||
import { Popover, PopoverContent, PopoverTrigger } from '@langgenius/dify-ui/popover'
|
||||
import { RiAddLine } from '@remixicon/react'
|
||||
import { useCallback, useState } from 'react'
|
||||
import { useCallback, useRef, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import VarReferenceVars from '@/app/components/workflow/nodes/_base/components/variable/var-reference-vars'
|
||||
|
||||
@@ -15,6 +15,7 @@ type ConditionAddProps = {
|
||||
}
|
||||
|
||||
const ConditionAdd = ({ className, variables, onSelectVariable, disabled }: ConditionAddProps) => {
|
||||
const searchInputRef = useRef<HTMLInputElement>(null)
|
||||
const { t } = useTranslation()
|
||||
const [open, setOpen] = useState(false)
|
||||
|
||||
@@ -40,12 +41,18 @@ const ConditionAdd = ({ className, variables, onSelectVariable, disabled }: Cond
|
||||
}}
|
||||
/>
|
||||
<PopoverContent
|
||||
initialFocus={searchInputRef}
|
||||
placement="bottom-start"
|
||||
sideOffset={4}
|
||||
className="border-none bg-transparent p-0 shadow-none backdrop-blur-none"
|
||||
>
|
||||
<div className="w-74 rounded-lg border-[0.5px] border-components-panel-border bg-components-panel-bg-blur shadow-lg">
|
||||
<VarReferenceVars vars={variables} isSupportFileVar onChange={handleSelectVariable} />
|
||||
<VarReferenceVars
|
||||
searchInputRef={searchInputRef}
|
||||
vars={variables}
|
||||
isSupportFileVar
|
||||
onChange={handleSelectVariable}
|
||||
/>
|
||||
</div>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
|
||||
+10
-1
@@ -6,6 +6,7 @@ import type {
|
||||
VarType,
|
||||
} from '@/app/components/workflow/types'
|
||||
import { Popover, PopoverContent, PopoverTrigger } from '@langgenius/dify-ui/popover'
|
||||
import { useRef } from 'react'
|
||||
import VariableTag from '@/app/components/workflow/nodes/_base/components/variable-tag'
|
||||
import VarReferenceVars from '@/app/components/workflow/nodes/_base/components/variable/var-reference-vars'
|
||||
|
||||
@@ -28,9 +29,11 @@ const ConditionVarSelector = ({
|
||||
nodesOutputVars,
|
||||
onChange,
|
||||
}: ConditionVarSelectorProps) => {
|
||||
const searchInputRef = useRef<HTMLInputElement>(null)
|
||||
return (
|
||||
<Popover open={open} onOpenChange={onOpenChange}>
|
||||
<PopoverTrigger
|
||||
// TODO: Declare non-native button semantics for this div trigger to support keyboard activation.
|
||||
render={
|
||||
<div className="cursor-pointer">
|
||||
<VariableTag
|
||||
@@ -43,12 +46,18 @@ const ConditionVarSelector = ({
|
||||
}
|
||||
/>
|
||||
<PopoverContent
|
||||
initialFocus={searchInputRef}
|
||||
placement="bottom-start"
|
||||
sideOffset={4}
|
||||
className="border-none bg-transparent p-0 shadow-none backdrop-blur-none"
|
||||
>
|
||||
<div className="w-74 rounded-lg border-[0.5px] border-components-panel-border bg-components-panel-bg-blur shadow-lg">
|
||||
<VarReferenceVars vars={nodesOutputVars} isSupportFileVar onChange={onChange} />
|
||||
<VarReferenceVars
|
||||
searchInputRef={searchInputRef}
|
||||
vars={nodesOutputVars}
|
||||
isSupportFileVar
|
||||
onChange={onChange}
|
||||
/>
|
||||
</div>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
|
||||
@@ -12,7 +12,7 @@ import { Popover, PopoverContent, PopoverTrigger } from '@langgenius/dify-ui/pop
|
||||
import { RiArrowDownSLine } from '@remixicon/react'
|
||||
import { useBoolean } from 'ahooks'
|
||||
import { capitalize } from 'es-toolkit/string'
|
||||
import { memo, useCallback, useState } from 'react'
|
||||
import { memo, useCallback, useRef, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { Variable02 } from '@/app/components/base/icons/src/vender/solid/development'
|
||||
import VarReferenceVars from '@/app/components/workflow/nodes/_base/components/variable/var-reference-vars'
|
||||
@@ -41,6 +41,7 @@ const ConditionNumberInput = ({
|
||||
isShort,
|
||||
unit,
|
||||
}: ConditionNumberInputProps) => {
|
||||
const searchInputRef = useRef<HTMLInputElement>(null)
|
||||
const { t } = useTranslation()
|
||||
const [numberVarTypeVisible, setNumberVarTypeVisible] = useState(false)
|
||||
const [variableSelectorVisible, setVariableSelectorVisible] = useState(false)
|
||||
@@ -106,6 +107,7 @@ const ConditionNumberInput = ({
|
||||
)}
|
||||
</PopoverTrigger>
|
||||
<PopoverContent
|
||||
initialFocus={searchInputRef}
|
||||
placement="bottom-start"
|
||||
sideOffset={2}
|
||||
className="border-none bg-transparent shadow-none"
|
||||
@@ -116,7 +118,11 @@ const ConditionNumberInput = ({
|
||||
isShort && 'w-50',
|
||||
)}
|
||||
>
|
||||
<VarReferenceVars vars={variables} onChange={handleSelectVariable} />
|
||||
<VarReferenceVars
|
||||
searchInputRef={searchInputRef}
|
||||
vars={variables}
|
||||
onChange={handleSelectVariable}
|
||||
/>
|
||||
</div>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
|
||||
@@ -69,13 +69,18 @@ describe('WorkflowGeneratorModal', () => {
|
||||
})
|
||||
|
||||
describe('Accessibility', () => {
|
||||
it('should expose the dialog title and instruction label', async () => {
|
||||
it('should expose the dialog title and focus the labeled instruction', async () => {
|
||||
render(<WorkflowGeneratorModal />)
|
||||
|
||||
expect(screen.getByRole('dialog', { name: /workflowGenerator\.title/i })).toBeInTheDocument()
|
||||
expect(
|
||||
screen.getByRole('textbox', { name: /workflowGenerator\.instruction/i }),
|
||||
).toBeInTheDocument()
|
||||
await waitFor(() =>
|
||||
expect(
|
||||
screen.getByRole('textbox', { name: /workflowGenerator\.instruction/i }),
|
||||
).toHaveFocus(),
|
||||
)
|
||||
})
|
||||
|
||||
it('should keep the instruction field keyboard-operable', async () => {
|
||||
|
||||
@@ -213,6 +213,7 @@ function WorkflowGeneratorModal() {
|
||||
// Seed from the palette's inline-captured instruction, else the last instruction
|
||||
// generated from (persisted across opens). Captured at mount only — the modal
|
||||
// remounts on each open, so this is just the initial value.
|
||||
const instructionRef = useRef<HTMLTextAreaElement>(null)
|
||||
const [instruction, setInstruction] = useState(initialInstruction || lastInstruction || '')
|
||||
// Planner result, streamed ahead of the graph (null until it lands).
|
||||
const [plan, setPlan] = useState<WorkflowGenPlan | null>(null)
|
||||
@@ -570,7 +571,10 @@ function WorkflowGeneratorModal() {
|
||||
}
|
||||
}}
|
||||
>
|
||||
<DialogContent className="h-[min(680px,calc(100dvh-2rem))] max-h-none! w-[calc(100vw-2rem)] max-w-285! min-w-0 overflow-hidden! border-none p-0! text-left align-middle">
|
||||
<DialogContent
|
||||
initialFocus={instructionRef}
|
||||
className="h-[min(680px,calc(100dvh-2rem))] max-h-none! w-[calc(100vw-2rem)] max-w-285! min-w-0 overflow-hidden! border-none p-0! text-left align-middle"
|
||||
>
|
||||
<div className="flex h-full min-h-0 flex-col md:flex-row">
|
||||
{/* Left pane: instructions + ideal output + model selector */}
|
||||
<div className="max-h-[55%] w-full shrink-0 overflow-y-auto border-b border-divider-regular p-6 md:h-full md:max-h-none md:w-1/2 md:border-r md:border-b-0 lg:w-142.5">
|
||||
@@ -605,10 +609,7 @@ function WorkflowGeneratorModal() {
|
||||
{t(($) => $['workflowGenerator.instruction'])}
|
||||
</FieldLabel>
|
||||
<Textarea
|
||||
// Autofocus is appropriate here: the modal's sole purpose is to
|
||||
// capture an instruction, so focusing it on open aids the flow.
|
||||
// oxlint-disable-next-line jsx-a11y/no-autofocus
|
||||
autoFocus
|
||||
ref={instructionRef}
|
||||
className="h-40"
|
||||
placeholder={
|
||||
isRefine
|
||||
|
||||
Reference in New Issue
Block a user