mirror of
https://github.com/langgenius/dify.git
synced 2026-09-04 07:53:20 +08:00
refactor(dify-ui): expose autocomplete popup anatomy (#40895)
This commit is contained in:
parent
394b43376a
commit
76a91eb433
@ -229,7 +229,7 @@ Convert Figma output such as `rounded-[var(--radius/sm, 6px)]` to the mapped Tai
|
||||
|
||||
## Overlay & portal contract
|
||||
|
||||
Overlay primitives render their floating surfaces inside a [Base UI Portal] attached to `document.body`. This is the Base UI default — see the upstream [Portals][Base UI Portal] docs for the underlying behavior. Convenience content components such as `DialogContent`, `PopoverContent`, and `SelectContent` own their portal internally; primitives with explicit portal anatomy such as `Drawer` expose the matching `DrawerPortal` part so consumers can compose the full Base UI structure.
|
||||
Overlay primitives render their floating surfaces inside a [Base UI Portal] attached to `document.body`. This is the Base UI default — see the upstream [Portals][Base UI Portal] docs for the underlying behavior. Convenience content components such as `DialogContent`, `PopoverContent`, and `SelectContent` own their portal internally. Primitives with explicit anatomy expose their constituent parts so consumers can compose the Base UI structure and put behavior on its owner.
|
||||
|
||||
### Root isolation requirement
|
||||
|
||||
|
||||
@ -3,7 +3,6 @@ import { render } from 'vitest-browser-react'
|
||||
import {
|
||||
Autocomplete,
|
||||
AutocompleteClear,
|
||||
AutocompleteContent,
|
||||
AutocompleteEmpty,
|
||||
AutocompleteGroup,
|
||||
AutocompleteGroupLabel,
|
||||
@ -13,6 +12,9 @@ import {
|
||||
AutocompleteItemIndicator,
|
||||
AutocompleteItemText,
|
||||
AutocompleteList,
|
||||
AutocompletePopup,
|
||||
AutocompletePortal,
|
||||
AutocompletePositioner,
|
||||
AutocompleteSeparator,
|
||||
AutocompleteStatus,
|
||||
AutocompleteTrigger,
|
||||
@ -41,28 +43,23 @@ const renderAutocomplete = ({
|
||||
<AutocompleteClear data-testid="clear" />
|
||||
<AutocompleteTrigger data-testid="trigger" />
|
||||
</AutocompleteInputGroup>
|
||||
<AutocompleteContent
|
||||
positionerProps={{
|
||||
role: 'group',
|
||||
'aria-label': 'autocomplete positioner',
|
||||
}}
|
||||
popupProps={{
|
||||
role: 'dialog',
|
||||
'aria-label': 'autocomplete popup',
|
||||
}}
|
||||
>
|
||||
<AutocompleteStatus data-testid="status">2 suggestions</AutocompleteStatus>
|
||||
<AutocompleteList role="listbox" aria-label="autocomplete list" data-testid="list">
|
||||
<AutocompleteItem value="workflow">
|
||||
<AutocompleteItemText>Workflow</AutocompleteItemText>
|
||||
<AutocompleteItemIndicator />
|
||||
</AutocompleteItem>
|
||||
<AutocompleteItem value="dataset">
|
||||
<AutocompleteItemText>Dataset</AutocompleteItemText>
|
||||
</AutocompleteItem>
|
||||
</AutocompleteList>
|
||||
<AutocompleteEmpty data-testid="empty">No suggestions</AutocompleteEmpty>
|
||||
</AutocompleteContent>
|
||||
<AutocompletePortal>
|
||||
<AutocompletePositioner role="group" aria-label="autocomplete positioner">
|
||||
<AutocompletePopup role="dialog" aria-label="autocomplete popup">
|
||||
<AutocompleteStatus data-testid="status">2 suggestions</AutocompleteStatus>
|
||||
<AutocompleteList role="listbox" aria-label="autocomplete list" data-testid="list">
|
||||
<AutocompleteItem value="workflow">
|
||||
<AutocompleteItemText>Workflow</AutocompleteItemText>
|
||||
<AutocompleteItemIndicator />
|
||||
</AutocompleteItem>
|
||||
<AutocompleteItem value="dataset">
|
||||
<AutocompleteItemText>Dataset</AutocompleteItemText>
|
||||
</AutocompleteItem>
|
||||
</AutocompleteList>
|
||||
<AutocompleteEmpty data-testid="empty">No suggestions</AutocompleteEmpty>
|
||||
</AutocompletePopup>
|
||||
</AutocompletePositioner>
|
||||
</AutocompletePortal>
|
||||
</React.Fragment>
|
||||
)}
|
||||
</Autocomplete>,
|
||||
@ -182,30 +179,34 @@ describe('Autocomplete wrappers', () => {
|
||||
.toHaveAttribute('data-align', 'start')
|
||||
})
|
||||
|
||||
it('should apply custom placement side and passthrough popup props', async () => {
|
||||
it('should apply custom placement and popup props to their owning parts', async () => {
|
||||
const onPopupClick = vi.fn()
|
||||
const screen = await renderWithSafeViewport(
|
||||
<Autocomplete open defaultValue="workflow" items={['workflow']}>
|
||||
<AutocompleteInputGroup>
|
||||
<AutocompleteInput aria-label="Search suggestions" />
|
||||
</AutocompleteInputGroup>
|
||||
<AutocompleteContent
|
||||
placement="top-end"
|
||||
sideOffset={12}
|
||||
alignOffset={6}
|
||||
positionerProps={{ role: 'group', 'aria-label': 'autocomplete positioner' }}
|
||||
popupProps={{
|
||||
role: 'dialog',
|
||||
'aria-label': 'autocomplete popup',
|
||||
onClick: onPopupClick,
|
||||
}}
|
||||
>
|
||||
<AutocompleteList role="listbox" aria-label="autocomplete list">
|
||||
<AutocompleteItem value="workflow">
|
||||
<AutocompleteItemText>Workflow</AutocompleteItemText>
|
||||
</AutocompleteItem>
|
||||
</AutocompleteList>
|
||||
</AutocompleteContent>
|
||||
<AutocompletePortal>
|
||||
<AutocompletePositioner
|
||||
placement="top-end"
|
||||
sideOffset={12}
|
||||
alignOffset={6}
|
||||
role="group"
|
||||
aria-label="autocomplete positioner"
|
||||
>
|
||||
<AutocompletePopup
|
||||
role="dialog"
|
||||
aria-label="autocomplete popup"
|
||||
onClick={onPopupClick}
|
||||
>
|
||||
<AutocompleteList role="listbox" aria-label="autocomplete list">
|
||||
<AutocompleteItem value="workflow">
|
||||
<AutocompleteItemText>Workflow</AutocompleteItemText>
|
||||
</AutocompleteItem>
|
||||
</AutocompleteList>
|
||||
</AutocompletePopup>
|
||||
</AutocompletePositioner>
|
||||
</AutocompletePortal>
|
||||
</Autocomplete>,
|
||||
)
|
||||
|
||||
@ -223,18 +224,27 @@ describe('Autocomplete wrappers', () => {
|
||||
<AutocompleteInputGroup>
|
||||
<AutocompleteInput aria-label="Search suggestions" />
|
||||
</AutocompleteInputGroup>
|
||||
<AutocompleteContent popupProps={{ role: 'dialog', 'aria-label': 'autocomplete popup' }}>
|
||||
<AutocompleteList role="listbox" aria-label="autocomplete list">
|
||||
<AutocompleteGroup items={['workflow']}>
|
||||
<AutocompleteGroupLabel className="custom-label">Resources</AutocompleteGroupLabel>
|
||||
<AutocompleteSeparator className="custom-separator" data-testid="separator" />
|
||||
<AutocompleteItem value="workflow" className="custom-item">
|
||||
<AutocompleteItemText className="custom-text">Workflow</AutocompleteItemText>
|
||||
<AutocompleteItemIndicator className="custom-indicator" data-testid="indicator" />
|
||||
</AutocompleteItem>
|
||||
</AutocompleteGroup>
|
||||
</AutocompleteList>
|
||||
</AutocompleteContent>
|
||||
<AutocompletePortal>
|
||||
<AutocompletePositioner>
|
||||
<AutocompletePopup role="dialog" aria-label="autocomplete popup">
|
||||
<AutocompleteList role="listbox" aria-label="autocomplete list">
|
||||
<AutocompleteGroup items={['workflow']}>
|
||||
<AutocompleteGroupLabel className="custom-label">
|
||||
Resources
|
||||
</AutocompleteGroupLabel>
|
||||
<AutocompleteSeparator className="custom-separator" data-testid="separator" />
|
||||
<AutocompleteItem value="workflow" className="custom-item">
|
||||
<AutocompleteItemText className="custom-text">Workflow</AutocompleteItemText>
|
||||
<AutocompleteItemIndicator
|
||||
className="custom-indicator"
|
||||
data-testid="indicator"
|
||||
/>
|
||||
</AutocompleteItem>
|
||||
</AutocompleteGroup>
|
||||
</AutocompleteList>
|
||||
</AutocompletePopup>
|
||||
</AutocompletePositioner>
|
||||
</AutocompletePortal>
|
||||
</Autocomplete>,
|
||||
)
|
||||
|
||||
@ -253,15 +263,19 @@ describe('Autocomplete wrappers', () => {
|
||||
<AutocompleteInputGroup>
|
||||
<AutocompleteInput aria-label="Search resources" />
|
||||
</AutocompleteInputGroup>
|
||||
<AutocompleteContent>
|
||||
<AutocompleteList<string>>
|
||||
{(item) => (
|
||||
<AutocompleteItem key={item} value={item}>
|
||||
<AutocompleteItemText>{item}</AutocompleteItemText>
|
||||
</AutocompleteItem>
|
||||
)}
|
||||
</AutocompleteList>
|
||||
</AutocompleteContent>
|
||||
<AutocompletePortal>
|
||||
<AutocompletePositioner>
|
||||
<AutocompletePopup>
|
||||
<AutocompleteList<string>>
|
||||
{(item) => (
|
||||
<AutocompleteItem key={item} value={item}>
|
||||
<AutocompleteItemText>{item}</AutocompleteItemText>
|
||||
</AutocompleteItem>
|
||||
)}
|
||||
</AutocompleteList>
|
||||
</AutocompletePopup>
|
||||
</AutocompletePositioner>
|
||||
</AutocompletePortal>
|
||||
</Autocomplete>,
|
||||
)
|
||||
|
||||
|
||||
@ -6,7 +6,6 @@ import {
|
||||
Autocomplete,
|
||||
AutocompleteClear,
|
||||
AutocompleteCollection,
|
||||
AutocompleteContent,
|
||||
AutocompleteEmpty,
|
||||
AutocompleteGroup,
|
||||
AutocompleteGroupLabel,
|
||||
@ -15,6 +14,9 @@ import {
|
||||
AutocompleteItem,
|
||||
AutocompleteItemText,
|
||||
AutocompleteList,
|
||||
AutocompletePopup,
|
||||
AutocompletePortal,
|
||||
AutocompletePositioner,
|
||||
AutocompleteSeparator,
|
||||
AutocompleteStatus,
|
||||
AutocompleteTrigger,
|
||||
@ -369,12 +371,16 @@ const BasicTagAutocomplete = ({ size = 'medium' }: { size?: 'small' | 'medium' |
|
||||
<AutocompleteClear size={size} />
|
||||
<AutocompleteTrigger size={size} />
|
||||
</AutocompleteInputGroup>
|
||||
<AutocompleteContent>
|
||||
<AutocompleteList<Suggestion>>
|
||||
{(item) => <TagSuggestionItem key={item.value} item={item} />}
|
||||
</AutocompleteList>
|
||||
<AutocompleteEmpty>No tag suggestion. Keep the typed value.</AutocompleteEmpty>
|
||||
</AutocompleteContent>
|
||||
<AutocompletePortal>
|
||||
<AutocompletePositioner>
|
||||
<AutocompletePopup>
|
||||
<AutocompleteList<Suggestion>>
|
||||
{(item) => <TagSuggestionItem key={item.value} item={item} />}
|
||||
</AutocompleteList>
|
||||
<AutocompleteEmpty>No tag suggestion. Keep the typed value.</AutocompleteEmpty>
|
||||
</AutocompletePopup>
|
||||
</AutocompletePositioner>
|
||||
</AutocompletePortal>
|
||||
</Autocomplete>
|
||||
)
|
||||
|
||||
@ -511,15 +517,16 @@ const AsyncSearchDemo = () => {
|
||||
<AutocompleteClear />
|
||||
<AutocompleteTrigger />
|
||||
</AutocompleteInputGroup>
|
||||
<AutocompleteContent
|
||||
portalProps={{ hidden: !status }}
|
||||
popupProps={{ 'aria-busy': isPending || undefined }}
|
||||
>
|
||||
<AutocompleteStatus>{status}</AutocompleteStatus>
|
||||
<AutocompleteList<Suggestion>>
|
||||
{(item) => <SuggestionItem key={item.value} item={item} />}
|
||||
</AutocompleteList>
|
||||
</AutocompleteContent>
|
||||
<AutocompletePortal hidden={!status}>
|
||||
<AutocompletePositioner>
|
||||
<AutocompletePopup aria-busy={isPending || undefined}>
|
||||
<AutocompleteStatus>{status}</AutocompleteStatus>
|
||||
<AutocompleteList<Suggestion>>
|
||||
{(item) => <SuggestionItem key={item.value} item={item} />}
|
||||
</AutocompleteList>
|
||||
</AutocompletePopup>
|
||||
</AutocompletePositioner>
|
||||
</AutocompletePortal>
|
||||
</Autocomplete>
|
||||
</div>
|
||||
)
|
||||
@ -642,29 +649,33 @@ const FuzzyMatchingDemo = () => {
|
||||
<AutocompleteClear />
|
||||
<AutocompleteTrigger />
|
||||
</AutocompleteInputGroup>
|
||||
<AutocompleteContent>
|
||||
<AutocompleteList<Suggestion>>
|
||||
{(item) => (
|
||||
<AutocompleteItem key={item.value} value={item}>
|
||||
{item.icon && (
|
||||
<span
|
||||
className={cn(item.icon, 'size-4 shrink-0 text-text-tertiary')}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<AutocompletePortal>
|
||||
<AutocompletePositioner>
|
||||
<AutocompletePopup>
|
||||
<AutocompleteList<Suggestion>>
|
||||
{(item) => (
|
||||
<AutocompleteItem key={item.value} value={item}>
|
||||
{item.icon && (
|
||||
<span
|
||||
className={cn(item.icon, 'size-4 shrink-0 text-text-tertiary')}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
)}
|
||||
<div className="min-w-0 grow">
|
||||
<AutocompleteItemText className="block px-0">
|
||||
<FuzzyHighlight text={item.label} query={value} />
|
||||
</AutocompleteItemText>
|
||||
<span className="block truncate system-xs-regular text-text-tertiary">
|
||||
{item.description}
|
||||
</span>
|
||||
</div>
|
||||
</AutocompleteItem>
|
||||
)}
|
||||
<div className="min-w-0 grow">
|
||||
<AutocompleteItemText className="block px-0">
|
||||
<FuzzyHighlight text={item.label} query={value} />
|
||||
</AutocompleteItemText>
|
||||
<span className="block truncate system-xs-regular text-text-tertiary">
|
||||
{item.description}
|
||||
</span>
|
||||
</div>
|
||||
</AutocompleteItem>
|
||||
)}
|
||||
</AutocompleteList>
|
||||
<AutocompleteEmpty>No workflow suggestion. Keep typing freely.</AutocompleteEmpty>
|
||||
</AutocompleteContent>
|
||||
</AutocompleteList>
|
||||
<AutocompleteEmpty>No workflow suggestion. Keep typing freely.</AutocompleteEmpty>
|
||||
</AutocompletePopup>
|
||||
</AutocompletePositioner>
|
||||
</AutocompletePortal>
|
||||
</Autocomplete>
|
||||
</div>
|
||||
)
|
||||
@ -729,12 +740,16 @@ export const InlineAutocomplete: Story = {
|
||||
<AutocompleteClear />
|
||||
<AutocompleteTrigger />
|
||||
</AutocompleteInputGroup>
|
||||
<AutocompleteContent>
|
||||
<AutocompleteList<Suggestion>>
|
||||
{(item) => <SuggestionItem key={item.value} item={item} dense />}
|
||||
</AutocompleteList>
|
||||
<AutocompleteEmpty>No inline completion. Continue typing freely.</AutocompleteEmpty>
|
||||
</AutocompleteContent>
|
||||
<AutocompletePortal>
|
||||
<AutocompletePositioner>
|
||||
<AutocompletePopup>
|
||||
<AutocompleteList<Suggestion>>
|
||||
{(item) => <SuggestionItem key={item.value} item={item} dense />}
|
||||
</AutocompleteList>
|
||||
<AutocompleteEmpty>No inline completion. Continue typing freely.</AutocompleteEmpty>
|
||||
</AutocompletePopup>
|
||||
</AutocompletePositioner>
|
||||
</AutocompletePortal>
|
||||
</Autocomplete>
|
||||
</div>
|
||||
),
|
||||
@ -761,10 +776,14 @@ export const GroupedSuggestions: Story = {
|
||||
<AutocompleteClear />
|
||||
<AutocompleteTrigger />
|
||||
</AutocompleteInputGroup>
|
||||
<AutocompleteContent popupClassName="w-[420px]">
|
||||
<GroupedSuggestionList />
|
||||
<AutocompleteEmpty>No suggestion. Use the text as entered.</AutocompleteEmpty>
|
||||
</AutocompleteContent>
|
||||
<AutocompletePortal>
|
||||
<AutocompletePositioner>
|
||||
<AutocompletePopup className="w-[420px]">
|
||||
<GroupedSuggestionList />
|
||||
<AutocompleteEmpty>No suggestion. Use the text as entered.</AutocompleteEmpty>
|
||||
</AutocompletePopup>
|
||||
</AutocompletePositioner>
|
||||
</AutocompletePortal>
|
||||
</Autocomplete>
|
||||
</div>
|
||||
),
|
||||
@ -796,15 +815,19 @@ export const LimitResults: Story = {
|
||||
<AutocompleteClear />
|
||||
<AutocompleteTrigger />
|
||||
</AutocompleteInputGroup>
|
||||
<AutocompleteContent popupClassName="w-[420px]">
|
||||
<AutocompleteStatus className="border-b border-divider-subtle">
|
||||
<LimitedStatus total={workflowSuggestions.length} />
|
||||
</AutocompleteStatus>
|
||||
<AutocompleteList<Suggestion>>
|
||||
{(item) => <SuggestionItem key={item.value} item={item} />}
|
||||
</AutocompleteList>
|
||||
<AutocompleteEmpty>No suggestion. Submit the typed text instead.</AutocompleteEmpty>
|
||||
</AutocompleteContent>
|
||||
<AutocompletePortal>
|
||||
<AutocompletePositioner>
|
||||
<AutocompletePopup className="w-[420px]">
|
||||
<AutocompleteStatus className="border-b border-divider-subtle">
|
||||
<LimitedStatus total={workflowSuggestions.length} />
|
||||
</AutocompleteStatus>
|
||||
<AutocompleteList<Suggestion>>
|
||||
{(item) => <SuggestionItem key={item.value} item={item} />}
|
||||
</AutocompleteList>
|
||||
<AutocompleteEmpty>No suggestion. Submit the typed text instead.</AutocompleteEmpty>
|
||||
</AutocompletePopup>
|
||||
</AutocompletePositioner>
|
||||
</AutocompletePortal>
|
||||
</Autocomplete>
|
||||
</div>
|
||||
),
|
||||
@ -867,11 +890,15 @@ const VirtualizedLongSuggestionsDemo = () => {
|
||||
<AutocompleteClear />
|
||||
<AutocompleteTrigger />
|
||||
</AutocompleteInputGroup>
|
||||
<AutocompleteContent popupClassName="w-[440px] p-1">
|
||||
<VirtualizedStatus />
|
||||
<VirtualizedSuggestionList virtualizerRef={virtualizerRef} />
|
||||
<AutocompleteEmpty>No suggestion. Free-form text is still valid.</AutocompleteEmpty>
|
||||
</AutocompleteContent>
|
||||
<AutocompletePortal>
|
||||
<AutocompletePositioner>
|
||||
<AutocompletePopup className="w-[440px] p-1">
|
||||
<VirtualizedStatus />
|
||||
<VirtualizedSuggestionList virtualizerRef={virtualizerRef} />
|
||||
<AutocompleteEmpty>No suggestion. Free-form text is still valid.</AutocompleteEmpty>
|
||||
</AutocompletePopup>
|
||||
</AutocompletePositioner>
|
||||
</AutocompletePortal>
|
||||
</Autocomplete>
|
||||
</div>
|
||||
)
|
||||
@ -907,12 +934,18 @@ export const Empty: Story = {
|
||||
<AutocompleteClear />
|
||||
<AutocompleteTrigger />
|
||||
</AutocompleteInputGroup>
|
||||
<AutocompleteContent>
|
||||
<AutocompleteList<Suggestion>>
|
||||
{(item) => <TagSuggestionItem key={item.value} item={item} />}
|
||||
</AutocompleteList>
|
||||
<AutocompleteEmpty>No tag suggestion. The custom text remains valid.</AutocompleteEmpty>
|
||||
</AutocompleteContent>
|
||||
<AutocompletePortal>
|
||||
<AutocompletePositioner>
|
||||
<AutocompletePopup>
|
||||
<AutocompleteList<Suggestion>>
|
||||
{(item) => <TagSuggestionItem key={item.value} item={item} />}
|
||||
</AutocompleteList>
|
||||
<AutocompleteEmpty>
|
||||
No tag suggestion. The custom text remains valid.
|
||||
</AutocompleteEmpty>
|
||||
</AutocompletePopup>
|
||||
</AutocompletePositioner>
|
||||
</AutocompletePortal>
|
||||
</Autocomplete>
|
||||
</div>
|
||||
),
|
||||
@ -933,11 +966,15 @@ export const DisabledAndReadOnly: Story = {
|
||||
<AutocompleteClear />
|
||||
<AutocompleteTrigger />
|
||||
</AutocompleteInputGroup>
|
||||
<AutocompleteContent>
|
||||
<AutocompleteList<Suggestion>>
|
||||
{(item) => <TagSuggestionItem key={item.value} item={item} />}
|
||||
</AutocompleteList>
|
||||
</AutocompleteContent>
|
||||
<AutocompletePortal>
|
||||
<AutocompletePositioner>
|
||||
<AutocompletePopup>
|
||||
<AutocompleteList<Suggestion>>
|
||||
{(item) => <TagSuggestionItem key={item.value} item={item} />}
|
||||
</AutocompleteList>
|
||||
</AutocompletePopup>
|
||||
</AutocompletePositioner>
|
||||
</AutocompletePortal>
|
||||
</Autocomplete>
|
||||
<Autocomplete
|
||||
items={promptCompletions}
|
||||
@ -951,11 +988,15 @@ export const DisabledAndReadOnly: Story = {
|
||||
<AutocompleteClear />
|
||||
<AutocompleteTrigger />
|
||||
</AutocompleteInputGroup>
|
||||
<AutocompleteContent>
|
||||
<AutocompleteList<Suggestion>>
|
||||
{(item) => <SuggestionItem key={item.value} item={item} />}
|
||||
</AutocompleteList>
|
||||
</AutocompleteContent>
|
||||
<AutocompletePortal>
|
||||
<AutocompletePositioner>
|
||||
<AutocompletePopup>
|
||||
<AutocompleteList<Suggestion>>
|
||||
{(item) => <SuggestionItem key={item.value} item={item} />}
|
||||
</AutocompleteList>
|
||||
</AutocompletePopup>
|
||||
</AutocompletePositioner>
|
||||
</AutocompletePortal>
|
||||
</Autocomplete>
|
||||
</div>
|
||||
),
|
||||
|
||||
@ -250,56 +250,46 @@ function AutocompleteIcon({ className, children, ...props }: AutocompleteIconPro
|
||||
)
|
||||
}
|
||||
|
||||
type AutocompleteContentProps = {
|
||||
children: React.ReactNode
|
||||
placement?: Placement
|
||||
sideOffset?: number
|
||||
alignOffset?: number
|
||||
const AutocompletePortal = BaseAutocomplete.Portal
|
||||
type AutocompletePortalProps = BaseAutocomplete.Portal.Props
|
||||
|
||||
type AutocompletePositionerProps = Omit<
|
||||
BaseAutocomplete.Positioner.Props,
|
||||
'className' | 'side' | 'align'
|
||||
> & {
|
||||
className?: string
|
||||
popupClassName?: string
|
||||
portalProps?: Omit<BaseAutocomplete.Portal.Props, 'children'>
|
||||
positionerProps?: Omit<
|
||||
BaseAutocomplete.Positioner.Props,
|
||||
'children' | 'className' | 'side' | 'align' | 'sideOffset' | 'alignOffset'
|
||||
>
|
||||
popupProps?: Omit<BaseAutocomplete.Popup.Props, 'children' | 'className'>
|
||||
placement?: Placement
|
||||
}
|
||||
|
||||
function AutocompleteContent({
|
||||
children,
|
||||
function AutocompletePositioner({
|
||||
className,
|
||||
placement = 'bottom-start',
|
||||
sideOffset = 4,
|
||||
alignOffset = 0,
|
||||
className,
|
||||
popupClassName,
|
||||
portalProps,
|
||||
positionerProps,
|
||||
popupProps,
|
||||
}: AutocompleteContentProps) {
|
||||
...props
|
||||
}: AutocompletePositionerProps) {
|
||||
const { side, align } = parsePlacement(placement)
|
||||
|
||||
return (
|
||||
<BaseAutocomplete.Portal {...portalProps}>
|
||||
<BaseAutocomplete.Positioner
|
||||
side={side}
|
||||
align={align}
|
||||
sideOffset={sideOffset}
|
||||
alignOffset={alignOffset}
|
||||
className={cn('z-50 outline-hidden', className)}
|
||||
{...positionerProps}
|
||||
>
|
||||
<BaseAutocomplete.Popup
|
||||
className={cn(
|
||||
autocompletePopupClassName,
|
||||
floatingPopupAnimationClassName,
|
||||
popupClassName,
|
||||
)}
|
||||
{...popupProps}
|
||||
>
|
||||
{children}
|
||||
</BaseAutocomplete.Popup>
|
||||
</BaseAutocomplete.Positioner>
|
||||
</BaseAutocomplete.Portal>
|
||||
<BaseAutocomplete.Positioner
|
||||
side={side}
|
||||
align={align}
|
||||
sideOffset={sideOffset}
|
||||
className={cn('z-50 outline-hidden', className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
type AutocompletePopupProps = Omit<BaseAutocomplete.Popup.Props, 'className'> & {
|
||||
className?: string
|
||||
}
|
||||
|
||||
function AutocompletePopup({ className, ...props }: AutocompletePopupProps) {
|
||||
return (
|
||||
<BaseAutocomplete.Popup
|
||||
className={cn(autocompletePopupClassName, floatingPopupAnimationClassName, className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
@ -405,7 +395,6 @@ export {
|
||||
Autocomplete,
|
||||
AutocompleteClear,
|
||||
AutocompleteCollection,
|
||||
AutocompleteContent,
|
||||
AutocompleteEmpty,
|
||||
AutocompleteGroup,
|
||||
AutocompleteGroupLabel,
|
||||
@ -416,6 +405,9 @@ export {
|
||||
AutocompleteItemIndicator,
|
||||
AutocompleteItemText,
|
||||
AutocompleteList,
|
||||
AutocompletePopup,
|
||||
AutocompletePortal,
|
||||
AutocompletePositioner,
|
||||
AutocompleteRow,
|
||||
AutocompleteSeparator,
|
||||
AutocompleteStatus,
|
||||
@ -429,7 +421,6 @@ export type {
|
||||
AutocompleteChangeEventDetails,
|
||||
AutocompleteClearProps,
|
||||
AutocompleteCollectionProps,
|
||||
AutocompleteContentProps,
|
||||
AutocompleteEmptyProps,
|
||||
AutocompleteFlatProps,
|
||||
AutocompleteGroupedProps,
|
||||
@ -442,6 +433,9 @@ export type {
|
||||
AutocompleteItemProps,
|
||||
AutocompleteItemTextProps,
|
||||
AutocompleteListProps,
|
||||
AutocompletePopupProps,
|
||||
AutocompletePortalProps,
|
||||
AutocompletePositionerProps,
|
||||
AutocompleteProps,
|
||||
AutocompleteRowProps,
|
||||
AutocompleteSeparatorProps,
|
||||
|
||||
@ -2,13 +2,15 @@ import type { AutocompleteChangeEventDetails } from '@langgenius/dify-ui/autocom
|
||||
import {
|
||||
Autocomplete,
|
||||
AutocompleteCollection,
|
||||
AutocompleteContent,
|
||||
AutocompleteEmpty,
|
||||
AutocompleteInput,
|
||||
AutocompleteInputGroup,
|
||||
AutocompleteItem,
|
||||
AutocompleteItemText,
|
||||
AutocompleteList,
|
||||
AutocompletePopup,
|
||||
AutocompletePortal,
|
||||
AutocompletePositioner,
|
||||
AutocompleteStatus,
|
||||
} from '@langgenius/dify-ui/autocomplete'
|
||||
import { Field, FieldLabel } from '@langgenius/dify-ui/field'
|
||||
@ -101,58 +103,63 @@ const InstitutionField = ({ value, onValueChange }: InstitutionFieldProps) => {
|
||||
placeholder={t(($) => $['form.schoolName.placeholder'], { ns: 'education' })}
|
||||
/>
|
||||
</AutocompleteInputGroup>
|
||||
<AutocompleteContent
|
||||
popupClassName="w-(--anchor-width) max-w-(--available-width)"
|
||||
portalProps={{ keepMounted: true }}
|
||||
popupProps={{ 'aria-busy': (shouldOpenPopup && isLoading) || undefined }}
|
||||
>
|
||||
<AutocompleteList ref={listRef}>
|
||||
<AutocompleteCollection<string>>
|
||||
{(institution) => (
|
||||
<AutocompleteItem key={institution} value={institution} title={institution}>
|
||||
<AutocompleteItemText>{institution}</AutocompleteItemText>
|
||||
</AutocompleteItem>
|
||||
)}
|
||||
</AutocompleteCollection>
|
||||
{footerState !== null ? (
|
||||
<div
|
||||
className="relative flex h-10 items-center justify-center px-3"
|
||||
aria-hidden="true"
|
||||
>
|
||||
{hasNextPage ? (
|
||||
<InfiniteScrollSentinel
|
||||
className="absolute inset-x-0 top-0"
|
||||
canLoadMore={canLoadMore}
|
||||
onLoadMore={() => {
|
||||
void fetchNextPage({ cancelRefetch: false })
|
||||
}}
|
||||
preloadDistance={5}
|
||||
scrollContainerRef={listRef}
|
||||
/>
|
||||
<AutocompletePortal keepMounted>
|
||||
<AutocompletePositioner>
|
||||
<AutocompletePopup
|
||||
className="w-(--anchor-width) max-w-(--available-width)"
|
||||
aria-busy={(shouldOpenPopup && isLoading) || undefined}
|
||||
>
|
||||
<AutocompleteList ref={listRef}>
|
||||
<AutocompleteCollection<string>>
|
||||
{(institution) => (
|
||||
<AutocompleteItem key={institution} value={institution} title={institution}>
|
||||
<AutocompleteItemText>{institution}</AutocompleteItemText>
|
||||
</AutocompleteItem>
|
||||
)}
|
||||
</AutocompleteCollection>
|
||||
{footerState !== null ? (
|
||||
<div
|
||||
className="relative flex h-10 items-center justify-center px-3"
|
||||
aria-hidden="true"
|
||||
>
|
||||
{hasNextPage ? (
|
||||
<InfiniteScrollSentinel
|
||||
className="absolute inset-x-0 top-0"
|
||||
canLoadMore={canLoadMore}
|
||||
onLoadMore={() => {
|
||||
void fetchNextPage({ cancelRefetch: false })
|
||||
}}
|
||||
preloadDistance={5}
|
||||
scrollContainerRef={listRef}
|
||||
/>
|
||||
) : null}
|
||||
{footerState === 'error' ? (
|
||||
<span className="system-sm-regular text-text-destructive">
|
||||
{t(($) => $['dynamicSelect.error'], { ns: 'common' })}
|
||||
</span>
|
||||
) : (
|
||||
<Loading className="h-10" />
|
||||
)}
|
||||
</div>
|
||||
) : null}
|
||||
{footerState === 'error' ? (
|
||||
<span className="system-sm-regular text-text-destructive">
|
||||
{t(($) => $['dynamicSelect.error'], { ns: 'common' })}
|
||||
</span>
|
||||
) : (
|
||||
<Loading className="h-10" />
|
||||
)}
|
||||
</div>
|
||||
) : null}
|
||||
</AutocompleteList>
|
||||
<AutocompleteEmpty>
|
||||
{shouldShowEmpty ? t(($) => $['form.schoolName.noResults'], { ns: 'education' }) : null}
|
||||
</AutocompleteEmpty>
|
||||
<AutocompleteStatus className="p-0">
|
||||
<span className="sr-only">
|
||||
{shouldOpenPopup && isLoading
|
||||
? t(($) => $.loading, { ns: 'common' })
|
||||
: footerState === 'error'
|
||||
? t(($) => $['dynamicSelect.error'], { ns: 'common' })
|
||||
</AutocompleteList>
|
||||
<AutocompleteEmpty>
|
||||
{shouldShowEmpty
|
||||
? t(($) => $['form.schoolName.noResults'], { ns: 'education' })
|
||||
: null}
|
||||
</span>
|
||||
</AutocompleteStatus>
|
||||
</AutocompleteContent>
|
||||
</AutocompleteEmpty>
|
||||
<AutocompleteStatus className="p-0">
|
||||
<span className="sr-only">
|
||||
{shouldOpenPopup && isLoading
|
||||
? t(($) => $.loading, { ns: 'common' })
|
||||
: footerState === 'error'
|
||||
? t(($) => $['dynamicSelect.error'], { ns: 'common' })
|
||||
: null}
|
||||
</span>
|
||||
</AutocompleteStatus>
|
||||
</AutocompletePopup>
|
||||
</AutocompletePositioner>
|
||||
</AutocompletePortal>
|
||||
</Autocomplete>
|
||||
</Field>
|
||||
)
|
||||
|
||||
Loading…
Reference in New Issue
Block a user