fix(web): simplify infotip icon variants (#38644)

This commit is contained in:
yyh 2026-07-10 14:37:36 +08:00 committed by GitHub
parent 4a8c6773a3
commit 489e77658e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
23 changed files with 108 additions and 78 deletions

View File

@ -149,7 +149,8 @@ export function WebAppSSONotEnabledTip() {
return (
<Infotip
aria-label={tip}
iconClassName="h-4 w-4 shrink-0 text-text-warning-secondary hover:text-text-warning-secondary"
className="text-text-warning-secondary hover:text-text-warning-secondary"
iconSize="large"
>
{tip}
</Infotip>

View File

@ -247,7 +247,6 @@ const ConfigContent: FC<Props> = ({
<Infotip
aria-label={option.tips}
className="ml-0.5 size-3.5"
iconClassName="h-3.5 w-3.5"
popupClassName="w-[200px]"
>
{option.tips}

View File

@ -756,16 +756,17 @@ describe('ChatInputArea', () => {
// -------------------------------------------------------------------------
describe('Feature Bar', () => {
it('should render footer notice with tooltip when provided', async () => {
it('should render footer notice with an accessible infotip', async () => {
const user = userEvent.setup({ delay: null })
const footerNotice = 'Agent runs in a Linux sandbox.'
const footerNoticeTooltip = 'For Dify Community Edition, each of your agents runs in a Linux 7.0.0-10060-aws sandbox environment within your docker. Your edits to the environment via Build Chats are persistent.'
const accessibleName = `common.operation.learnMore: ${footerNotice}`
render(<ChatInputArea visionConfig={mockVisionConfig} footerNotice={footerNotice} footerNoticeTooltip={footerNoticeTooltip} />)
expect(screen.getByText(footerNotice)).toBeInTheDocument()
expect(screen.queryByText(footerNoticeTooltip)).not.toBeInTheDocument()
await user.hover(screen.getByRole('button', { name: footerNoticeTooltip }))
await user.click(screen.getByRole('button', { name: accessibleName }))
expect(await screen.findByText(footerNoticeTooltip)).toBeInTheDocument()
})

View File

@ -4,7 +4,6 @@ import type { EnableType, OnSend } from '../../types'
import type { InputForm } from '../type'
import type { FileUpload } from '@/app/components/base/features/types'
import { cn } from '@langgenius/dify-ui/cn'
import { Popover, PopoverContent, PopoverTrigger } from '@langgenius/dify-ui/popover'
import { toast } from '@langgenius/dify-ui/toast'
import { noop } from 'es-toolkit/function'
import { decode } from 'html-entities'
@ -16,6 +15,7 @@ import FeatureBar from '@/app/components/base/features/new-feature-panel/feature
import { FileListInChatInput } from '@/app/components/base/file-uploader'
import { useFile } from '@/app/components/base/file-uploader/hooks'
import { FileContextProvider, useFileStore } from '@/app/components/base/file-uploader/store'
import { Infotip } from '@/app/components/base/infotip'
import VoiceInput from '@/app/components/base/voice-input'
import { TransferMethod } from '@/types/app'
import { useCheckInputsForms } from '../check-input-forms-hooks'
@ -170,6 +170,10 @@ const ChatInputArea = ({ readonly, botName, customPlaceholder, showFeatureBar, s
const operation = (<Operation ref={holdSpaceRef} readonly={readonly} fileConfig={visionConfig} speechToTextConfig={speechToTextConfig} onShowVoiceInput={handleShowVoiceInput} onSend={handleSend} sendButtonLabel={sendButtonLabel} sendButtonLoading={sendButtonLoading} disabled={!canSend} theme={theme} />)
const shouldShowFooterNotice = footerNotice !== undefined && footerNotice !== null
const shouldShowFooterNoticeTooltip = footerNoticeTooltip !== undefined && footerNoticeTooltip !== null
const footerNoticeText = typeof footerNotice === 'string' ? footerNotice.trim() : ''
const footerNoticeAriaLabel = footerNoticeText
? `${t('operation.learnMore', { ns: 'common' })}: ${footerNoticeText}`
: t('operation.learnMore', { ns: 'common' })
return (
<>
<div className={cn('pointer-events-auto relative z-10 overflow-hidden rounded-xl border border-components-chat-input-border bg-components-panel-bg-blur pb-[9px] shadow-md', isDragActive && 'border border-dashed border-components-option-card-option-selected-border', disabled && 'pointer-events-none border-components-panel-border opacity-50 shadow-none')}>
@ -212,31 +216,16 @@ const ChatInputArea = ({ readonly, botName, customPlaceholder, showFeatureBar, s
{shouldShowFooterNotice && (
<div className="m-1 mt-0 -translate-y-2 rounded-b-[10px] border-r border-b border-l border-components-panel-border-subtle bg-util-colors-indigo-indigo-50 px-2.5 py-2 pt-4">
<div className="flex items-center gap-1">
<span aria-hidden className="i-ri-information-line size-3.5 shrink-0 text-text-accent" />
<div className="body-xs-medium text-text-accent">{footerNotice}</div>
<div className="min-w-0 flex-1 body-xs-medium text-text-accent">{footerNotice}</div>
{shouldShowFooterNoticeTooltip && (
<Popover>
<PopoverTrigger
openOnHover
delay={300}
closeDelay={200}
render={(
<button
type="button"
className="ml-auto flex size-5 items-center justify-center rounded-md system-xs-medium text-text-accent hover:bg-state-base-hover focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-components-button-primary-bg"
aria-label={typeof footerNoticeTooltip === 'string' ? footerNoticeTooltip : undefined}
>
<span aria-hidden className="i-ri-question-line size-3.5 shrink-0" />
</button>
)}
/>
<PopoverContent
placement="top"
popupClassName="max-w-80 rounded-md border-0 px-3 py-2 text-start system-xs-regular wrap-break-word text-text-tertiary"
>
{footerNoticeTooltip}
</PopoverContent>
</Popover>
<Infotip
aria-label={footerNoticeAriaLabel}
className="ml-auto size-5 rounded-md text-text-accent hover:bg-state-base-hover hover:text-text-accent"
iconVariant="information"
popupClassName="max-w-80 border-0 text-start wrap-break-word"
>
{footerNoticeTooltip}
</Infotip>
)}
</div>
</div>

View File

@ -373,7 +373,7 @@ describe('OpeningSettingModal', () => {
expect(screen.queryByText(/openingStatement\.openingQuestionDescription/)).not.toBeInTheDocument()
})
it('should show the opening questions description in a tooltip', async () => {
it('should show the opening questions description in an infotip', async () => {
await render(
<OpeningSettingModal
data={defaultData}
@ -382,11 +382,9 @@ describe('OpeningSettingModal', () => {
/>,
)
act(() => {
fireEvent.mouseEnter(screen.getByRole('button', { name: /openingStatement\.openingQuestionDescription/ }))
})
await userEvent.hover(screen.getByRole('button', { name: /openingStatement\.openingQuestionDescription/ }))
expect(screen.getByText(/openingStatement\.openingQuestionDescription/)).toBeInTheDocument()
expect(await screen.findByText(/openingStatement\.openingQuestionDescription/)).toBeInTheDocument()
})
it('should call onAutoAddPromptVariable when confirm add is clicked', async () => {

View File

@ -121,7 +121,6 @@ const OpeningSettingModal = ({
aria-label={t('openingStatement.openingQuestionDescription', { ns: 'appDebug' })}
className="size-3.5"
popupClassName="max-w-[220px] system-sm-regular text-text-secondary"
delay={0}
>
{t('openingStatement.openingQuestionDescription', { ns: 'appDebug' })}
</Infotip>

View File

@ -0,0 +1,53 @@
import type { ReactNode } from 'react'
import { render, screen, waitFor } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { createPortal } from 'react-dom'
import { Infotip } from '../index'
function ClickBoundary({ children, onClick }: { children: ReactNode, onClick: () => void }) {
return (
<button type="button" aria-label="Parent action" onClick={onClick}>
{createPortal(children, document.body)}
</button>
)
}
describe('Infotip', () => {
it('should open with keyboard activation', async () => {
const user = userEvent.setup()
render(<Infotip aria-label="Rate limits">Rate limit details</Infotip>)
screen.getByRole('button', { name: 'Rate limits' }).focus()
await user.keyboard('{Enter}')
expect(await screen.findByRole('dialog')).toHaveTextContent('Rate limit details')
})
it('should close the dialog with Escape', async () => {
const user = userEvent.setup()
render(<Infotip aria-label="Rate limits">Rate limit details</Infotip>)
await user.click(screen.getByRole('button', { name: 'Rate limits' }))
expect(await screen.findByRole('dialog')).toBeInTheDocument()
await user.keyboard('{Escape}')
await waitFor(() => {
expect(screen.queryByRole('dialog')).not.toBeInTheDocument()
})
})
it('should not propagate trigger clicks', async () => {
const user = userEvent.setup()
const parentClick = vi.fn()
render(
<ClickBoundary onClick={parentClick}>
<Infotip aria-label="Rate limits">Rate limit details</Infotip>
</ClickBoundary>,
)
await user.click(screen.getByRole('button', { name: 'Rate limits' }))
expect(parentClick).not.toHaveBeenCalled()
})
})

View File

@ -1,12 +1,25 @@
'use client'
import type { Placement } from '@langgenius/dify-ui/popover'
import type { MouseEvent, ReactNode } from 'react'
import { cn } from '@langgenius/dify-ui/cn'
import { Popover, PopoverContent, PopoverTrigger } from '@langgenius/dify-ui/popover'
const iconClassNames = {
question: 'i-ri-question-line',
information: 'i-ri-information-line',
} as const
const iconSizeClassNames = {
small: 'size-3',
medium: 'size-3.5',
large: 'size-4',
} as const
type InfotipIconVariant = keyof typeof iconClassNames
type InfotipIconSize = keyof typeof iconSizeClassNames
/**
* Infotip a `?` icon that reveals a long-form explanation on hover / focus / tap.
* Infotip an info glyph that reveals a long-form explanation on hover / keyboard activation / tap.
*
* Implements the pattern Base UI calls an "infotip":
* https://base-ui.com/react/components/tooltip#infotips
@ -32,34 +45,25 @@ import { Popover, PopoverContent, PopoverTrigger } from '@langgenius/dify-ui/pop
type InfotipProps = {
/** Popup content. Rich nodes are allowed. */
'children': ReactNode
/** Accessible name for the trigger. Required; should match the popup text. */
/** Accessible name for the icon-only trigger. */
'aria-label': string
/** Placement of the popup relative to the trigger. Defaults to `top`. */
'placement'?: Placement
/** Distance between the trigger and popup. Defaults to the popover primitive spacing. */
'sideOffset'?: number
/** Extra classes on the outer trigger wrapper (layout / margin). */
/** Extra classes on the trigger for contextual layout and color. */
'className'?: string
/** Extra classes on the `?` icon itself (size / color overrides). */
'iconClassName'?: string
/** Icon glyph. Defaults to `question`. */
'iconVariant'?: InfotipIconVariant
/** Icon size. Defaults to `medium` (14px). */
'iconSize'?: InfotipIconSize
/** Extra classes on the popup body (width / padding / whitespace overrides). */
'popupClassName'?: string
/** Hover open delay in ms. Defaults to 300 to match the app-wide Tooltip delay. */
'delay'?: number
/** Hover close delay in ms. Defaults to 200 to match the app-wide Tooltip delay. */
'closeDelay'?: number
}
export function Infotip({
children,
'aria-label': ariaLabel,
placement = 'top',
sideOffset,
className,
iconClassName,
iconVariant = 'question',
iconSize = 'medium',
popupClassName,
delay = 300,
closeDelay = 200,
}: InfotipProps) {
const handleClick = (event: MouseEvent<HTMLButtonElement>) => {
event.stopPropagation()
@ -69,20 +73,19 @@ export function Infotip({
<Popover>
<PopoverTrigger
openOnHover
delay={delay}
closeDelay={closeDelay}
delay={300}
closeDelay={200}
aria-label={ariaLabel}
onClick={handleClick}
className={cn(
'inline-flex size-4 shrink-0 cursor-pointer items-center justify-center rounded-sm border-0 bg-transparent p-0 outline-hidden focus-visible:ring-2 focus-visible:ring-state-accent-solid',
'inline-flex size-4 shrink-0 cursor-pointer items-center justify-center rounded-sm border-0 bg-transparent p-0 text-text-quaternary outline-hidden hover:text-text-tertiary focus-visible:ring-2 focus-visible:ring-state-accent-solid',
className,
)}
>
<span aria-hidden className={cn('i-ri-question-line size-3.5 text-text-quaternary hover:text-text-tertiary', iconClassName)} />
<span aria-hidden className={cn(iconClassNames[iconVariant], iconSizeClassNames[iconSize])} />
</PopoverTrigger>
<PopoverContent
placement={placement}
sideOffset={sideOffset}
placement="top"
popupClassName={cn('max-w-[300px] rounded-md px-3 py-2 system-xs-regular text-text-tertiary', popupClassName)}
>
{children}

View File

@ -196,7 +196,6 @@ export const GeneralChunkingOptions: FC<GeneralChunkingOptionsProps> = ({
<Infotip
aria-label={t('stepTwo.QATip', { ns: 'datasetCreation' })}
className="size-3.5"
iconClassName="h-full w-full"
>
{t('stepTwo.QATip', { ns: 'datasetCreation' })}
</Infotip>

View File

@ -142,8 +142,7 @@ const EditMetadataBatchModal: FC<Props> = ({ datasetId, documentNum, list, onSav
</label>
<Infotip
aria-label={t(`${i18nPrefix}.applyToAllSelectDocumentTip`, { ns: 'dataset' })}
className="p-px"
iconClassName="size-3.5 text-text-tertiary"
className="p-px text-text-tertiary"
popupClassName="max-w-[240px]"
>
{t(`${i18nPrefix}.applyToAllSelectDocumentTip`, { ns: 'dataset' })}

View File

@ -156,7 +156,7 @@ const ModelLoadBalancingConfigs = ({
<Infotip
aria-label={t('modelProvider.loadBalancingInfo', { ns: 'common' })}
className="size-3"
iconClassName="h-full w-full"
iconSize="small"
popupClassName="max-w-[300px]"
>
{t('modelProvider.loadBalancingInfo', { ns: 'common' })}

View File

@ -128,7 +128,6 @@ export const CreateSubscriptionButton = ({ buttonType = CreateButtonType.FULL_BU
<Infotip
aria-label={t('subscription.addType.options.manual.tip', { ns: 'pluginTrigger' })}
className="size-3.5"
iconClassName="h-full w-full"
>
{t('subscription.addType.options.manual.tip', { ns: 'pluginTrigger' })}
</Infotip>

View File

@ -34,7 +34,6 @@ export const SubscriptionListView: React.FC<SubscriptionListViewProps> = ({
<Infotip
aria-label={t('subscription.list.tip', { ns: 'pluginTrigger' })}
className="size-3.5"
iconClassName="h-full w-full"
>
{t('subscription.list.tip', { ns: 'pluginTrigger' })}
</Infotip>

View File

@ -37,7 +37,6 @@ export const SubscriptionSelectorView: React.FC<SubscriptionSelectorProps> = ({
<Infotip
aria-label={t('subscription.list.tip', { ns: 'pluginTrigger' })}
className="size-3.5"
iconClassName="h-full w-full"
>
{t('subscription.list.tip', { ns: 'pluginTrigger' })}
</Infotip>

View File

@ -75,7 +75,6 @@ const InfoTooltip = ({ children }: { children: string }) => {
<Infotip
aria-label={children}
className="ml-1 size-3.5"
iconClassName="h-3.5 w-3.5"
popupClassName="w-[180px]"
>
{children}

View File

@ -303,7 +303,6 @@ export function SearchMethodOption({
<Infotip
aria-label={rerankModelTip}
className="ml-0.5 size-3.5 shrink-0"
iconClassName="h-3.5 w-3.5"
>
{rerankModelTip}
</Infotip>

View File

@ -72,7 +72,6 @@ export function TopKAndScoreThreshold({
<Infotip
aria-label={topKTip}
className="ml-0.5 size-3.5"
iconClassName="h-3.5 w-3.5"
>
{topKTip}
</Infotip>
@ -115,7 +114,6 @@ export function TopKAndScoreThreshold({
<Infotip
aria-label={scoreThresholdTip}
className="ml-0.5 size-3.5"
iconClassName="h-3.5 w-3.5"
>
{scoreThresholdTip}
</Infotip>

View File

@ -83,7 +83,6 @@ const PromptEditor: FC<PromptEditorProps> = ({
<Infotip
aria-label={t('nodes.llm.jsonSchema.promptTooltip', { ns: 'workflow' })}
className="size-3.5"
iconClassName="h-full w-full"
>
{t('nodes.llm.jsonSchema.promptTooltip', { ns: 'workflow' })}
</Infotip>

View File

@ -134,7 +134,6 @@ const Panel: FC<NodePanelProps<ParameterExtractorNodeType>> = ({
<Infotip
aria-label={t(`${i18nPrefix}.instructionTip`, { ns: 'workflow' })}
className="ml-0.5 size-3.5"
iconClassName="h-3.5 w-3.5"
popupClassName="w-[120px]"
>
{t(`${i18nPrefix}.instructionTip`, { ns: 'workflow' })}

View File

@ -51,7 +51,6 @@ const AdvancedSetting: FC<Props> = ({
<Infotip
aria-label={t(`${i18nPrefix}.instructionTip`, { ns: 'workflow' })}
className="ml-0.5 size-3.5"
iconClassName="h-3.5 w-3.5"
popupClassName="w-[120px]"
>
{t(`${i18nPrefix}.instructionTip`, { ns: 'workflow' })}

View File

@ -60,7 +60,7 @@ const MonthlyDaysSelector = ({ selectedDays, onChange }: MonthlyDaysSelectorProp
<Infotip
aria-label={t('nodes.triggerSchedule.lastDayTooltip', { ns: 'workflow' })}
className="mr-1 size-3"
iconClassName="h-3 w-3"
iconSize="small"
>
{t('nodes.triggerSchedule.lastDayTooltip', { ns: 'workflow' })}
</Infotip>

View File

@ -132,7 +132,6 @@ const VariableModal = ({
<Infotip
aria-label={t('env.modal.secretTip', { ns: 'workflow' })}
className="ml-0.5 size-3.5"
iconClassName="h-3.5 w-3.5"
popupClassName="w-[240px]"
>
{t('env.modal.secretTip', { ns: 'workflow' })}

View File

@ -321,8 +321,8 @@ function AgentBuildNoteInfotip() {
return (
<Infotip
aria-label={t('agentDetail.configure.files.buildNote.tooltip')}
className="size-5"
iconClassName="size-4 text-text-quaternary hover:text-text-quaternary"
className="size-5 text-text-quaternary hover:text-text-quaternary"
iconSize="large"
popupClassName="w-[230px] rounded-xl bg-components-tooltip-bg px-4 py-3.5 text-text-secondary shadow-lg backdrop-blur-[5px]"
>
<p className="body-xs-regular text-text-secondary">