mirror of
https://github.com/langgenius/dify.git
synced 2026-10-04 13:47:55 +08:00
fix(web): use concise accessible names for infotips (#42751)
Co-authored-by: zhsama <torvalds@linux.do>
This commit is contained in:
1 parent
0f6a6227f5
commit
c9e59a5590
146 files changed
+761
-812
No files matched your search
@@ -64,6 +64,7 @@ Upstream behavior remains owned by the [Base UI documentation].
|
||||
| ------------- | ------------------------------------------------------------------------------------------- |
|
||||
| [Button] | Action semantics, submit and link choices, loading versus disabled, and content spacing. |
|
||||
| [Icon Button] | Accessible names, decorative glyphs, appearance ownership, and primitive composition. |
|
||||
| [Infotip] | Explanation triggers, concise dialog names, and hint surface ownership. |
|
||||
| [Input Group] | Compound input anatomy, shared-surface ownership, DOM order, focus, and interactive addons. |
|
||||
|
||||
### Cross-component guides
|
||||
@@ -89,6 +90,7 @@ For a known contract, go directly to its guide above.
|
||||
[Button]: ./src/button/README.md
|
||||
[Forms]: ./docs/forms.md
|
||||
[Icon Button]: ./src/icon-button/README.md
|
||||
[Infotip]: ./src/infotip/README.md
|
||||
[Input Group]: ./src/input-group/README.md
|
||||
[Overlays]: ./docs/overlays.md
|
||||
[Public API authoring]: ./docs/authoring.md
|
||||
|
||||
@@ -126,12 +126,9 @@ with a second label or a competing error association:
|
||||
```
|
||||
|
||||
Use `DialogTitle` and, when useful, `DialogDescription` for a short dialog summary. Do not turn a
|
||||
whole form or rich dialog body into one description. Per [Base UI Tooltip guidance], Tooltip is a
|
||||
supplemental visual label, not the trigger's accessible-name source. Base UI specifically recommends
|
||||
an `aria-label` that closely matches the Tooltip content; apply that to icon-only triggers. When
|
||||
persistent visible trigger text already supplies the name, preserve the content-derived name per
|
||||
W3C and MDN guidance instead of adding a redundant override merely because Tooltip is present. Use
|
||||
[Overlay] choices for essential, structured, interactive, or touch-reachable information.
|
||||
whole form or rich dialog body into one description. Tooltip content is not the trigger's
|
||||
accessible-name source: icon-only triggers still need their own name, while persistent visible
|
||||
trigger text should continue to name the control.
|
||||
|
||||
Prefer descriptions associated with DOM text. When considering `aria-description`, verify target
|
||||
browser and assistive-technology behavior. The [AccName 1.2 working draft] gives
|
||||
@@ -159,7 +156,6 @@ that source computes to an empty description. Do not stack mechanisms to force r
|
||||
|
||||
[APG]: https://www.w3.org/WAI/ARIA/apg/practices/names-and-descriptions#namingtechniques
|
||||
[AccName 1.2 working draft]: https://www.w3.org/TR/accname-1.2#mapping_additional_nd_description
|
||||
[Base UI Tooltip guidance]: https://base-ui.com/react/components/tooltip#usage-guidelines
|
||||
[Base UI accessibility]: https://base-ui.com/react/overview/accessibility
|
||||
[Button]: ../src/button/README.md
|
||||
[Forms]: forms.md
|
||||
|
||||
@@ -64,8 +64,10 @@ Toast remains one layer above ordinary overlays.
|
||||
- Use `AlertDialog` only for a destructive or must-confirm decision requiring an explicit answer.
|
||||
- Use `Drawer` for side-panel interactions that follow the drawer model.
|
||||
- Use `DropdownMenu` for button-triggered action lists and `ContextMenu` for context actions.
|
||||
- Use [`Tooltip`] only for a short, non-interactive visual label. The trigger already needs an
|
||||
accessible name; use Popover for information users must reach on touch.
|
||||
- Use [`Tooltip`] for a supplementary visual label on a control that already has a primary
|
||||
action and an accessible name. Use `Infotip` when the information icon's action is to open
|
||||
the explanation. Text length alone does not determine the component. Follow the
|
||||
[Infotip contract] for its composition and accessible names.
|
||||
- Use [`PreviewCard`] as a non-interactive enhancement for a link destination. Essential
|
||||
information must also exist without the preview.
|
||||
- Use [`Popover`] for explanatory, structured, or interactive content that users must reach on
|
||||
@@ -76,6 +78,7 @@ renders a non-button element, set `nativeButton={false}`. Let the primitive own
|
||||
spacing unless its API documents a measured exception.
|
||||
|
||||
[Base UI Portal]: https://base-ui.com/react/overview/quick-start#portals
|
||||
[Infotip contract]: ../src/infotip/README.md
|
||||
[MDN `isolation`]: https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/Properties/isolation
|
||||
[`Dialog.Portal`]: https://base-ui.com/react/components/dialog#portal
|
||||
[`Popover`]: https://base-ui.com/react/components/popover
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
# Infotip
|
||||
|
||||
Use `Infotip` when an information icon's primary action is opening an explanation. Use
|
||||
`Tooltip` when the trigger already has another primary action and only needs a visual hint.
|
||||
|
||||
## Naming
|
||||
|
||||
Prefer an existing visible topic. It can name both the trigger and the dialog:
|
||||
|
||||
```tsx
|
||||
function ProcessingHint() {
|
||||
const labelId = useId()
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-1">
|
||||
<span id={labelId}>Document processing</span>
|
||||
<Infotip>
|
||||
<InfotipTrigger aria-labelledby={labelId} />
|
||||
<InfotipContent aria-labelledby={labelId}>
|
||||
Documents are processed in the order they are received.
|
||||
</InfotipContent>
|
||||
</Infotip>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
If the popup has its own visible title, use `InfotipTitle`. It gives the dialog its accessible name:
|
||||
|
||||
```tsx
|
||||
<Infotip>
|
||||
<InfotipTrigger aria-label="Processing priority" />
|
||||
<InfotipContent>
|
||||
<InfotipTitle>Processing priority</InfotipTitle>
|
||||
<p>Priority determines which documents are processed first.</p>
|
||||
</InfotipContent>
|
||||
</Infotip>
|
||||
```
|
||||
|
||||
Keep explanations, links, and other structured content as normal popup children rather than
|
||||
turning the whole body into the dialog description.
|
||||
|
||||
A few rules:
|
||||
|
||||
- The trigger's name must exist while the popup is closed.
|
||||
- Prefer `aria-labelledby` when a suitable visible topic already exists; otherwise use a short
|
||||
localized `aria-label`.
|
||||
- Sharing one short topic between the trigger and dialog is valid. Do not copy the full explanation
|
||||
into their accessible names.
|
||||
- Do not derive names from React children or automatically describe the dialog with its entire body.
|
||||
|
||||
## Visual ownership
|
||||
|
||||
Infotip owns its hint surface, spacing, typography, radius, and shadow. Callers may constrain width
|
||||
or preserve intentional line breaks.
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { Meta, StoryObj } from '@storybook/react-vite'
|
||||
import * as React from 'react'
|
||||
import { expect, userEvent, waitFor, within } from 'storybook/test'
|
||||
import { Infotip, InfotipContent, InfotipTrigger } from '.'
|
||||
import { Infotip, InfotipContent, InfotipTitle, InfotipTrigger } from '.'
|
||||
import { Button } from '../button'
|
||||
|
||||
const meta = {
|
||||
@@ -12,7 +12,7 @@ const meta = {
|
||||
docs: {
|
||||
description: {
|
||||
component:
|
||||
'An information button that opens explanatory content using Popover semantics. Compose the root, icon trigger, and content. Name the trigger and the dialog; use aria-labelledby when a visible heading exists. The popup remains available to keyboard and touch users and may contain links.',
|
||||
'An information button whose primary action is opening an explanation. Unlike Tooltip, it uses Popover semantics and supports click, touch, keyboard, and links. Name the trigger and dialog with a short topic, preferably referencing an existing visible label. Keep the explanation in the body; do not copy it into aria-label.',
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -21,19 +21,24 @@ const meta = {
|
||||
export default meta
|
||||
type Story = StoryObj<typeof meta>
|
||||
|
||||
export const Default: Story = {
|
||||
render: () => (
|
||||
function ProcessingHint() {
|
||||
const labelId = React.useId()
|
||||
return (
|
||||
<div className="flex items-center gap-1 text-text-secondary">
|
||||
Document processing
|
||||
<span id={labelId}>Document processing</span>
|
||||
<Infotip>
|
||||
<InfotipTrigger aria-label="Document processing" />
|
||||
<InfotipContent aria-label="Document processing">
|
||||
<InfotipTrigger aria-labelledby={labelId} />
|
||||
<InfotipContent aria-labelledby={labelId}>
|
||||
Documents are processed in the order they are received. Larger files can take longer to
|
||||
finish indexing.
|
||||
</InfotipContent>
|
||||
</Infotip>
|
||||
</div>
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
export const Default: Story = {
|
||||
render: () => <ProcessingHint />,
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement)
|
||||
const body = within(canvasElement.ownerDocument.body)
|
||||
@@ -75,6 +80,22 @@ export const WithLink: Story = {
|
||||
},
|
||||
}
|
||||
|
||||
function HeadingHint() {
|
||||
return (
|
||||
<Infotip defaultOpen>
|
||||
<InfotipTrigger aria-label="Processing priority" />
|
||||
<InfotipContent>
|
||||
<InfotipTitle>Processing priority</InfotipTitle>
|
||||
<p>Priority determines which documents are processed first.</p>
|
||||
</InfotipContent>
|
||||
</Infotip>
|
||||
)
|
||||
}
|
||||
|
||||
export const WithHeading: Story = {
|
||||
render: () => <HeadingHint />,
|
||||
}
|
||||
|
||||
function ControlledExample() {
|
||||
const [open, setOpen] = React.useState(false)
|
||||
return (
|
||||
|
||||
@@ -22,7 +22,9 @@ const iconSizeClassNames = {
|
||||
} as const
|
||||
|
||||
const Infotip = BasePopover.Root
|
||||
const InfotipTitle = BasePopover.Title
|
||||
type InfotipProps<Payload = unknown> = BasePopover.Root.Props<Payload>
|
||||
type InfotipTitleProps = BasePopover.Title.Props
|
||||
|
||||
type InfotipTriggerProps<Payload = unknown> = Omit<
|
||||
BasePopover.Trigger.Props<Payload>,
|
||||
@@ -103,5 +105,5 @@ function InfotipContent({
|
||||
)
|
||||
}
|
||||
|
||||
export { Infotip, InfotipContent, InfotipTrigger }
|
||||
export type { InfotipContentProps, InfotipProps, InfotipTriggerProps }
|
||||
export { Infotip, InfotipContent, InfotipTitle, InfotipTrigger }
|
||||
export type { InfotipContentProps, InfotipProps, InfotipTitleProps, InfotipTriggerProps }
|
||||
@@ -22,7 +22,7 @@ const meta = {
|
||||
docs: {
|
||||
description: {
|
||||
component:
|
||||
'Compound tooltip built on Base UI Tooltip. Wrap the app in `TooltipProvider` (done automatically in these stories) so multiple tooltips share open/close delays. Each tooltip pairs a `TooltipTrigger` with a `TooltipContent` and supports placement and offsets.\n\n**Usage contract** (mirrors the [Base UI tooltip guidelines](https://base-ui.com/react/components/tooltip#alternatives-to-tooltips)):\n\n- Tooltips are **supplementary visual labels** for sighted mouse and keyboard users. They are disabled on touch devices and are not announced to screen readers.\n- The trigger **must carry its own `aria-label` or visible text** that matches the tooltip — the tooltip does not replace labeling.\n- Keep content short and non-interactive (an icon-button label, a keyboard shortcut, one-word clarification).\n- **Do not** place descriptions, prose, links, or interactive controls inside a tooltip — touch and screen-reader users cannot reach them.\n- For hover-triggered rich previews that users move their cursor onto, use `PreviewCard` (dwell-able, structured content).\n- For an info icon that explains a concept (an "infotip"), or for any hover popup that needs interactive content or to reach touch/assistive-tech users, use `Popover` with `openOnHover` on the trigger.',
|
||||
'Compound tooltip built on Base UI Tooltip. Wrap the app in `TooltipProvider` so nested tooltips share open and close delays. Each tooltip pairs a `TooltipTrigger` with `TooltipContent` and supports placement and offsets.',
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -45,7 +45,7 @@ export const IconButton: Story = {
|
||||
docs: {
|
||||
description: {
|
||||
story:
|
||||
'The canonical tooltip use case: an icon-only button surfaces its accessible label as a tooltip for sighted mouse and keyboard users. The trigger already carries `aria-label` — the tooltip mirrors that label visually; it does **not** replace it.',
|
||||
'Icon-only actions keep their own accessible name while the tooltip provides the matching visual label.',
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -71,8 +71,7 @@ export const KeyboardShortcut: Story = {
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
story:
|
||||
'A short, supplementary hint that surfaces a keyboard shortcut next to a visible button label. The trigger is fully self-describing ("Save"); the tooltip only adds non-essential extra clarity for mouse/keyboard users.',
|
||||
story: 'A visible button label can use a tooltip for a supplementary keyboard shortcut.',
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -8,25 +8,6 @@ import { resolveClassName } from '../internals/resolve-class-name'
|
||||
import { hintPopupClassName } from '../overlay-shared'
|
||||
import { parsePlacement } from '../placement'
|
||||
|
||||
/**
|
||||
* Tooltip is an **ephemeral hint** tied to a trigger (typically an icon button,
|
||||
* badge, or short label). It follows Base UI's Tooltip semantics:
|
||||
*
|
||||
* - Opens on pointer hover or keyboard focus on the trigger.
|
||||
* - Closes as soon as the pointer leaves the trigger — the popup itself is
|
||||
* **not dwell-able**; users cannot move their cursor onto the tooltip.
|
||||
* - Must contain only short, non-interactive text. No links, buttons, form
|
||||
* controls, or structured panels.
|
||||
*
|
||||
* If you need any of the following, use `PreviewCard` instead (hover-triggered
|
||||
* rich preview that users can move their cursor onto):
|
||||
*
|
||||
* - Multi-line or structured content (icon + title + metadata)
|
||||
* - Content the user needs to "stop and read" for more than ~1 second
|
||||
* - Content wider than ~300px
|
||||
*
|
||||
* If you need interactive affordances (buttons, links, forms) use `Popover`.
|
||||
*/
|
||||
const TooltipProvider = BaseTooltip.Provider
|
||||
const Tooltip = BaseTooltip.Root
|
||||
const TooltipTrigger = BaseTooltip.Trigger
|
||||
|
||||
Reference in new issue
Block a user