mirror of
https://github.com/langgenius/dify.git
synced 2026-07-21 10:38:32 +08:00
fix(web): simplify infotip icon variants (#38644)
This commit is contained in:
parent
4a8c6773a3
commit
489e77658e
@ -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>
|
||||
|
||||
@ -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}
|
||||
|
||||
@ -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()
|
||||
})
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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 () => {
|
||||
|
||||
@ -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>
|
||||
|
||||
53
web/app/components/base/infotip/__tests__/index.spec.tsx
Normal file
53
web/app/components/base/infotip/__tests__/index.spec.tsx
Normal 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()
|
||||
})
|
||||
})
|
||||
@ -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}
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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' })}
|
||||
|
||||
@ -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' })}
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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}
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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' })}
|
||||
|
||||
@ -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' })}
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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' })}
|
||||
|
||||
@ -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">
|
||||
|
||||
Loading…
Reference in New Issue
Block a user