refactor(editor): Migrate mapper popover to ruka UI (#19564)

This commit is contained in:
Suguru Inoue
2025-09-17 10:42:40 +02:00
committed by GitHub
parent ae1af1101b
commit 0173d8f707
13 changed files with 212 additions and 566 deletions

View File

@@ -113,4 +113,32 @@ describe('N8nPopoverReka', () => {
expect(wrapper.props('maxHeight')).toBeUndefined();
});
describe('auto-focus behavior', () => {
it('should focus an element in the content slot by default', async () => {
const wrapper = render(N8nPopoverReka, {
props: { open: true },
slots: {
trigger: '<button />',
content: '<input />',
},
});
const popover = await wrapper.findByRole('dialog');
expect(popover.contains(document.activeElement)).toBe(true);
});
it('should suppress auto-focus when suppressAutoFocus is true', async () => {
const wrapper = render(N8nPopoverReka, {
props: { open: true, suppressAutoFocus: true },
slots: {
trigger: '<button />',
content: '<input />',
},
});
const popover = await wrapper.findByRole('dialog');
expect(popover.contains(document.activeElement)).toBe(false);
});
});
});

View File

@@ -1,14 +1,31 @@
<script setup lang="ts">
import { PopoverContent, PopoverPortal, PopoverRoot, PopoverTrigger } from 'reka-ui';
import {
PopoverContent,
type PopoverContentProps,
PopoverPortal,
PopoverRoot,
type PopoverRootProps,
PopoverTrigger,
} from 'reka-ui';
import type { CSSProperties } from 'vue';
import N8nScrollArea from '../N8nScrollArea/N8nScrollArea.vue';
interface Props {
open?: boolean;
interface Props
extends Pick<PopoverContentProps, 'side' | 'align' | 'sideFlip' | 'sideOffset' | 'reference'>,
Pick<PopoverRootProps, 'open'> {
/**
* Whether to enable scrolling in the popover content
*/
enableScrolling?: boolean;
/**
* Whether to enable slide-in animation
*/
enableSlideIn?: boolean;
/**
* Whether to suppress auto-focus behavior when the content includes focusable element
*/
suppressAutoFocus?: boolean;
/**
* Scrollbar visibility behavior
*/
@@ -17,14 +34,18 @@ interface Props {
* Popover width
*/
width?: string;
/**
* z-index of popover content
*/
zIndex?: number | CSSProperties['zIndex'];
/**
* Popover max height
*/
maxHeight?: string;
/**
* The preferred alignment against the trigger. May change when collisions occur.
* Additional class name set to PopperContent
*/
align?: 'start' | 'center' | 'end';
contentClass?: string;
}
interface Emits {
@@ -32,15 +53,24 @@ interface Emits {
}
const props = withDefaults(defineProps<Props>(), {
open: undefined,
maxHeight: undefined,
width: undefined,
enableScrolling: true,
enableSlideIn: true,
scrollType: 'hover',
align: undefined,
sideOffset: 5,
sideFlip: undefined,
suppressAutoFocus: false,
zIndex: 999,
});
const emit = defineEmits<Emits>();
function handleOpenAutoFocus(e: Event) {
if (props.suppressAutoFocus) {
e.preventDefault();
}
}
</script>
<template>
@@ -49,21 +79,29 @@ const emit = defineEmits<Emits>();
<slot name="trigger"></slot>
</PopoverTrigger>
<PopoverPortal>
<PopoverContent side="bottom" :align="align" :side-offset="5" :class="$style.popoverContent">
<PopoverContent
role="dialog"
:side="side"
:side-flip="sideFlip"
:align="align"
:side-offset="sideOffset"
:class="[$style.popoverContent, contentClass, { [$style.enableSlideIn]: enableSlideIn }]"
:style="{ width, zIndex }"
:reference="reference"
@open-auto-focus="handleOpenAutoFocus"
>
<N8nScrollArea
v-if="enableScrolling"
:max-height="props.maxHeight"
:max-height="maxHeight"
:type="scrollType"
:enable-vertical-scroll="true"
:enable-horizontal-scroll="false"
>
<div :style="{ width }">
<slot name="content" :close="() => emit('update:open', false)" />
</div>
</N8nScrollArea>
<div v-else :style="{ width }">
<slot name="content" :close="() => emit('update:open', false)" />
</div>
</N8nScrollArea>
<template v-else>
<slot name="content" :close="() => emit('update:open', false)" />
</template>
</PopoverContent>
</PopoverPortal>
</PopoverRoot>
@@ -77,10 +115,12 @@ const emit = defineEmits<Emits>();
box-shadow:
rgba(0, 0, 0, 0.1) 0 10px 15px -3px,
rgba(0, 0, 0, 0.05) 0 4px 6px -2px;
animation-duration: 400ms;
animation-timing-function: cubic-bezier(0.16, 1, 0.3, 1);
will-change: transform, opacity;
z-index: 999;
&.enableSlideIn {
animation-duration: 400ms;
animation-timing-function: cubic-bezier(0.16, 1, 0.3, 1);
}
}
.popoverContent[data-state='open'][data-side='top'] {

View File

@@ -4,13 +4,11 @@ exports[`N8nPopoverReka > should render correctly with default props 1`] = `
"<mock-popover-root>
<mock-popover-trigger><button></button></mock-popover-trigger>
<mock-popover-portal>
<mock-popover-content>
<mock-popover-content role="dialog">
<div dir="ltr" style="position: relative; --reka-scroll-area-corner-width: 0px; --reka-scroll-area-corner-height: 0px;" class="scrollAreaRoot">
<div data-reka-scroll-area-viewport="" style="overflow-x: hidden; overflow-y: hidden;" class="viewport" tabindex="0">
<div>
<div>
<content></content>
</div>
<content></content>
</div>
</div>
<style>