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:
yyh
2026-07-18 09:56:36 +00:00
committed by GitHub
co-authored by autofix-ci[bot]
parent fa65f45bff
commit 2ec34b2cfb
82 changed files with 1719 additions and 1679 deletions
@@ -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>
+7 -5
View File
@@ -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 = {