mirror of
https://github.com/langgenius/dify.git
synced 2026-09-21 05:11:22 +08:00
feat(workflow): improve block selector navigation and previews (#39212)
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
This commit is contained in:
@@ -12,7 +12,7 @@ const meta = {
|
||||
docs: {
|
||||
description: {
|
||||
component:
|
||||
'Unstyled Base UI Collapsible primitive. The examples mirror the official Root, Trigger, and Panel anatomy, with presentation supplied at the call site using Dify UI tokens.',
|
||||
'Styled Dify disclosure wrapper over Base UI Collapsible. It preserves the official Root, Trigger, and Panel anatomy while providing Dify layout, focus, disabled, and motion styles.',
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -10,91 +10,56 @@ describe('PreviewCardContent', () => {
|
||||
it('should use bottom placement and default offsets when placement props are not provided', async () => {
|
||||
const screen = await renderWithSafeViewport(
|
||||
<PreviewCard open>
|
||||
<PreviewCardTrigger
|
||||
render={
|
||||
<button type="button" aria-label="preview trigger">
|
||||
Open
|
||||
</button>
|
||||
}
|
||||
/>
|
||||
<PreviewCardTrigger href="#default-preview">Open</PreviewCardTrigger>
|
||||
<PreviewCardContent
|
||||
positionerProps={{ role: 'group', 'aria-label': 'default positioner' }}
|
||||
popupProps={{ role: 'dialog', 'aria-label': 'default popup' }}
|
||||
positionerProps={{ id: 'default-positioner' }}
|
||||
popupProps={{ id: 'default-popup' }}
|
||||
>
|
||||
<span>Default content</span>
|
||||
</PreviewCardContent>
|
||||
</PreviewCard>,
|
||||
)
|
||||
|
||||
await expect
|
||||
.element(screen.getByRole('group', { name: 'default positioner' }))
|
||||
.toHaveAttribute('data-side', 'bottom')
|
||||
await expect
|
||||
.element(screen.getByRole('group', { name: 'default positioner' }))
|
||||
.toHaveAttribute('data-align', 'center')
|
||||
await expect
|
||||
.element(screen.getByRole('dialog', { name: 'default popup' }))
|
||||
.toHaveTextContent('Default content')
|
||||
await expect.element(screen.getByText('Default content')).toBeInTheDocument()
|
||||
expect(document.getElementById('default-positioner')).toHaveAttribute('data-side', 'bottom')
|
||||
expect(document.getElementById('default-positioner')).toHaveAttribute('data-align', 'center')
|
||||
expect(document.getElementById('default-popup')).toHaveTextContent('Default content')
|
||||
})
|
||||
|
||||
it('should apply parsed custom placement and custom offsets when placement props are provided', async () => {
|
||||
const screen = await renderWithSafeViewport(
|
||||
<PreviewCard open>
|
||||
<PreviewCardTrigger
|
||||
render={
|
||||
<button type="button" aria-label="preview trigger">
|
||||
Open
|
||||
</button>
|
||||
}
|
||||
/>
|
||||
<PreviewCardTrigger href="#custom-preview">Open</PreviewCardTrigger>
|
||||
<PreviewCardContent
|
||||
placement="top-end"
|
||||
sideOffset={14}
|
||||
alignOffset={6}
|
||||
positionerProps={{ role: 'group', 'aria-label': 'custom positioner' }}
|
||||
popupProps={{ role: 'dialog', 'aria-label': 'custom popup' }}
|
||||
positionerProps={{ id: 'custom-positioner' }}
|
||||
popupProps={{ id: 'custom-popup' }}
|
||||
>
|
||||
<span>Custom placement content</span>
|
||||
</PreviewCardContent>
|
||||
</PreviewCard>,
|
||||
)
|
||||
|
||||
await expect
|
||||
.element(screen.getByRole('group', { name: 'custom positioner' }))
|
||||
.toHaveAttribute('data-side', 'top')
|
||||
await expect
|
||||
.element(screen.getByRole('group', { name: 'custom positioner' }))
|
||||
.toHaveAttribute('data-align', 'end')
|
||||
await expect
|
||||
.element(screen.getByRole('dialog', { name: 'custom popup' }))
|
||||
.toHaveTextContent('Custom placement content')
|
||||
await expect.element(screen.getByText('Custom placement content')).toBeInTheDocument()
|
||||
expect(document.getElementById('custom-positioner')).toHaveAttribute('data-side', 'top')
|
||||
expect(document.getElementById('custom-positioner')).toHaveAttribute('data-align', 'end')
|
||||
expect(document.getElementById('custom-popup')).toHaveTextContent('Custom placement content')
|
||||
})
|
||||
})
|
||||
|
||||
describe('Passthrough props', () => {
|
||||
it('should forward positionerProps and popupProps when passthrough props are provided', async () => {
|
||||
const onPopupClick = vi.fn()
|
||||
|
||||
const screen = await render(
|
||||
<PreviewCard open>
|
||||
<PreviewCardTrigger
|
||||
render={
|
||||
<button type="button" aria-label="preview trigger">
|
||||
Open
|
||||
</button>
|
||||
}
|
||||
/>
|
||||
<PreviewCardTrigger href="#passthrough-preview">Open</PreviewCardTrigger>
|
||||
<PreviewCardContent
|
||||
positionerProps={{
|
||||
role: 'group',
|
||||
'aria-label': 'preview positioner',
|
||||
id: 'preview-positioner-id',
|
||||
}}
|
||||
popupProps={{
|
||||
id: 'preview-popup-id',
|
||||
role: 'dialog',
|
||||
'aria-label': 'preview content',
|
||||
onClick: onPopupClick,
|
||||
}}
|
||||
>
|
||||
<span>Preview body</span>
|
||||
@@ -102,40 +67,26 @@ describe('PreviewCardContent', () => {
|
||||
</PreviewCard>,
|
||||
)
|
||||
|
||||
const popup = screen.getByRole('dialog', { name: 'preview content' })
|
||||
await popup.click()
|
||||
|
||||
await expect
|
||||
.element(screen.getByRole('group', { name: 'preview positioner' }))
|
||||
.toHaveAttribute('id', 'preview-positioner-id')
|
||||
await expect.element(popup).toHaveAttribute('id', 'preview-popup-id')
|
||||
expect(onPopupClick).toHaveBeenCalledTimes(1)
|
||||
await expect.element(screen.getByText('Preview body')).toBeInTheDocument()
|
||||
expect(document.getElementById('preview-positioner-id')).toBeInTheDocument()
|
||||
expect(document.getElementById('preview-popup-id')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
describe('Trigger click behavior', () => {
|
||||
it('should forward the trigger click to the consumer handler so the primary action runs', async () => {
|
||||
const onPrimaryClick = vi.fn()
|
||||
|
||||
describe('Trigger semantics', () => {
|
||||
it('should preserve the link destination', async () => {
|
||||
const screen = await renderWithSafeViewport(
|
||||
<PreviewCard>
|
||||
<PreviewCardTrigger
|
||||
render={
|
||||
<button type="button" aria-label="preview trigger" onClick={onPrimaryClick}>
|
||||
Open
|
||||
</button>
|
||||
}
|
||||
/>
|
||||
<PreviewCardContent popupProps={{ role: 'dialog', 'aria-label': 'preview content' }}>
|
||||
<PreviewCardTrigger href="/preview-destination">Preview destination</PreviewCardTrigger>
|
||||
<PreviewCardContent>
|
||||
<span>Preview body</span>
|
||||
</PreviewCardContent>
|
||||
</PreviewCard>,
|
||||
)
|
||||
|
||||
const trigger = screen.getByRole('button', { name: 'preview trigger' })
|
||||
await trigger.click()
|
||||
|
||||
expect(onPrimaryClick).toHaveBeenCalledTimes(1)
|
||||
await expect
|
||||
.element(screen.getByRole('link', { name: 'Preview destination' }))
|
||||
.toHaveAttribute('href', '/preview-destination')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -3,9 +3,6 @@ import type { Placement } from '.'
|
||||
import * as React from 'react'
|
||||
import { createPreviewCardHandle, PreviewCard, PreviewCardContent, PreviewCardTrigger } from '.'
|
||||
|
||||
const rowButtonClassName =
|
||||
'flex w-full items-center gap-2 rounded-lg px-3 py-2 text-left text-sm text-text-secondary outline-hidden hover:bg-state-base-hover focus-visible:ring-2 focus-visible:ring-state-accent-solid'
|
||||
|
||||
const triggerButtonClassName =
|
||||
'rounded-lg border border-divider-subtle bg-components-button-secondary-bg px-3 py-1.5 text-sm text-text-secondary shadow-xs outline-hidden hover:bg-state-base-hover focus-visible:ring-2 focus-visible:ring-state-accent-solid'
|
||||
|
||||
@@ -20,7 +17,7 @@ const meta = {
|
||||
docs: {
|
||||
description: {
|
||||
component:
|
||||
"Hover- and focus-activated rich preview for triggers whose primary click has its own destination (following a link, selecting a row, jumping to a definition). Built on Base UI PreviewCard.\n\n**A11y contract:** touch and screen-reader users cannot open the preview. Never place information or actions in the popup that are not also reachable from the trigger's primary click destination. If that is unavoidable, add a separate click affordance (Popover) or move the unique content onto the destination.",
|
||||
'Hover- and focus-activated rich link preview built on Base UI PreviewCard.\n\n**A11y contract:** touch and screen-reader users cannot open the preview. Keep popup content available on the link destination. A polymorphic action trigger is a Dify application-level extension and is only valid when its click result exposes the same information.',
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -85,38 +82,6 @@ export const LinkPreview: Story = {
|
||||
),
|
||||
}
|
||||
|
||||
export const Supplementary: Story = {
|
||||
name: 'Supplementary preview on a button trigger',
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
story:
|
||||
'Application-level adaptation of the same semantic: the trigger is a `<button>` that owns a primary action (selecting a model row) rather than an `<a>`. The preview still only shows supplementary info reachable from the selection destination, so the a11y contract holds.',
|
||||
},
|
||||
},
|
||||
},
|
||||
render: () => (
|
||||
<PreviewCard>
|
||||
<PreviewCardTrigger
|
||||
render={
|
||||
<button type="button" className={rowButtonClassName}>
|
||||
<span className="i-ri-sparkling-fill h-4 w-4 text-text-accent" />
|
||||
<span>gpt-4o</span>
|
||||
</button>
|
||||
}
|
||||
/>
|
||||
<PreviewCardContent placement="right" popupClassName="w-[220px] p-3">
|
||||
<div className="flex flex-col gap-2">
|
||||
<div className="text-sm font-medium text-text-primary">gpt-4o</div>
|
||||
<div className="text-xs text-text-tertiary">
|
||||
Multimodal flagship model. Vision, audio and 128k context.
|
||||
</div>
|
||||
</div>
|
||||
</PreviewCardContent>
|
||||
</PreviewCard>
|
||||
),
|
||||
}
|
||||
|
||||
const PLACEMENTS: Placement[] = [
|
||||
'top-start',
|
||||
'top',
|
||||
@@ -152,13 +117,9 @@ const PlacementsDemo = () => {
|
||||
))}
|
||||
</div>
|
||||
<PreviewCard open>
|
||||
<PreviewCardTrigger
|
||||
render={
|
||||
<button type="button" className={triggerButtonClassName}>
|
||||
Hover me
|
||||
</button>
|
||||
}
|
||||
/>
|
||||
<PreviewCardTrigger href="#preview-card-placement" className={triggerButtonClassName}>
|
||||
Hover me
|
||||
</PreviewCardTrigger>
|
||||
<PreviewCardContent placement={placement} popupClassName="w-56 p-3">
|
||||
<div className="flex flex-col gap-1">
|
||||
<div className="text-sm font-semibold text-text-primary">
|
||||
@@ -187,12 +148,11 @@ const CustomDelayDemo = () => (
|
||||
<PreviewCardTrigger
|
||||
delay={100}
|
||||
closeDelay={100}
|
||||
render={
|
||||
<button type="button" className={triggerButtonClassName}>
|
||||
Snappy trigger
|
||||
</button>
|
||||
}
|
||||
/>
|
||||
href="#preview-card-delay"
|
||||
className={triggerButtonClassName}
|
||||
>
|
||||
Snappy trigger
|
||||
</PreviewCardTrigger>
|
||||
<PreviewCardContent popupClassName="w-64 p-3">
|
||||
<div className="flex flex-col gap-1">
|
||||
<div className="text-sm font-semibold text-text-primary">Fast hover</div>
|
||||
|
||||
@@ -11,20 +11,22 @@ export type { Placement }
|
||||
|
||||
/**
|
||||
* PreviewCard is a hover/focus-triggered rich preview intended to supplement a
|
||||
* trigger whose primary action is its own click destination (e.g. a link, a
|
||||
* selectable row, a chip that jumps to a definition).
|
||||
* link. Base UI's canonical trigger renders an anchor.
|
||||
*
|
||||
* A11y contract — match Base UI's guidance:
|
||||
* - The popup MUST NOT contain information or actions that are not also
|
||||
* reachable from the trigger's primary click destination. Touch and screen
|
||||
* reader users cannot open the card and must be able to get the same
|
||||
* information/actions without it.
|
||||
* reachable from the link destination. Touch and screen reader users cannot
|
||||
* open the card and must be able to get the same information/actions without
|
||||
* it.
|
||||
* - A polymorphic action trigger is an application-level extension and is only
|
||||
* valid when its primary click result exposes the same information.
|
||||
* - If content is unique to the popup, either (a) add a separate click-triggered
|
||||
* affordance (Popover) next to the trigger, or (b) move the unique content
|
||||
* onto the click destination.
|
||||
*/
|
||||
export const PreviewCard = BasePreviewCard.Root
|
||||
export const PreviewCardTrigger = BasePreviewCard.Trigger
|
||||
export const PreviewCardViewport = BasePreviewCard.Viewport
|
||||
export const createPreviewCardHandle = BasePreviewCard.createHandle
|
||||
|
||||
type PreviewCardContentProps = {
|
||||
|
||||
Reference in New Issue
Block a user